Elements

Elements · Structured output

Flow graph

Work as a graph rather than a list: branches that fan out and rejoin.

intake
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-flow-graph"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

Flow graph draws work as a small dependency graph instead of a flat list: nodes sit in columns, and curved edges connect the ones that depend on each other. With a runtime you lay it out from a tool call's arguments as the model writes them; standalone you hold the node and edge arrays yourself.

Getting started

With a runtime, a flow graph is usually one tool call whose arguments describe an entire plan: a list of steps, each naming what it depends on. A small layout function turns that list into the columns, rows, and edges FlowGraph expects.

Lay out the plan from the tool call

components/assistant-ui/elements/plan-graph-tool-ui.tsx
"use client";

import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import {
  FlowGraph,
  type FlowEdge,
  type FlowNode,
} from "@/components/assistant-ui/elements/flow-graph";

type PlanStep = { id: string; label: string; dependsOn?: string[] };
type PlanArgs = { steps: PlanStep[] };

function layout(steps: PlanStep[], done: boolean) {
  const columnOf = new Map<string, number>();
  const rowsUsed = new Map<number, number>();
  const nodes: FlowNode[] = steps.map((step, index) => {
    const deps = step.dependsOn ?? [];
    const column = deps.length
      ? Math.max(...deps.map((id) => columnOf.get(id) ?? 0)) + 1
      : 0;
    columnOf.set(step.id, column);
    const row = rowsUsed.get(column) ?? 0;
    rowsUsed.set(column, row + 1);
    const state = done || index < steps.length - 1 ? "done" : "active";
    return { id: step.id, label: step.label, column, row, state };
  });
  const edges: FlowEdge[] = steps.flatMap((step) =>
    (step.dependsOn ?? []).map((from) => ({ from, to: step.id })),
  );
  return { nodes, edges };
}

export const PlanGraphToolUI: ToolCallMessagePartComponent<
  PlanArgs,
  unknown
> = ({ args, status }) => {
  const steps = args.steps ?? [];
  if (steps.length === 0) return null;
  const { nodes, edges } = layout(steps, status.type === "complete");
  return <FlowGraph nodes={nodes} edges={edges} visibleCount={nodes.length} />;
};

Register the tool

app/toolkit.ts
import { defineToolkit } from "@assistant-ui/react";
import { z } from "zod";
import { PlanGraphToolUI } from "@/components/assistant-ui/elements/plan-graph-tool-ui";

const step = z.object({
  id: z.string(),
  label: z.string(),
  dependsOn: z.array(z.string()).optional(),
});

export const toolkit = defineToolkit({
  propose_plan: {
    type: "frontend",
    description: "Propose a dependency-ordered plan for the work ahead.",
    parameters: z.object({ steps: z.array(step) }),
    execute: async ({ steps }) => runPlan(steps),
    render: PlanGraphToolUI,
  },
});
app/MyRuntimeProvider.tsx
import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";
import { toolkit } from "./toolkit";

export function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
  const runtime = useChatRuntime();
  const config = AuiConfig({ tools: Tools({ toolkit }) });
  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      {children}
    </AssistantRuntimeProvider>
  );
}

See Tool UI for backend-defined tools and approval gates.

Anatomy

<div data-slot="flow-graph">
  <svg>{/* one curved path per edge */}</svg>
  <div>{/* one absolutely positioned node per visible node, fading and scaling in */}</div>
</div>

visibleCount slices nodes from the start of the array. An edge draws with a bright stroke only once both its from and to ids are inside that slice, and reads as a faint stroke otherwise; an edge naming an id that isn't in nodes is skipped entirely. state drives each node's border and fill: "done" is a muted filled box, "active" is a blue outline, and "pending" is a dashed, dimmer box.

Examples

Positioning nodes

column and row are grid coordinates you choose, not derived from edges; two nodes can share a column, and the graph does not try to avoid overlaps for you.

{ id: "search", label: "search", column: 2, row: 0, state: "done" },
{ id: "patch", label: "patch", column: 2, row: 2, state: "active" },

Restyle the graph

Both lanes take className on the root. Node labels read the shared mono token and the root reads paper, both from surfaces.tsx.

<FlowGraph className="max-w-none" /* ... */ />

API reference

Tool-call render props

PropTypeDescription
argsTArgsParsed arguments. Partial while the model is still streaming them.
argsTextstringRaw JSON argument text streamed by the model.
resultTResult | undefinedThe tool's return value once it completes. undefined while running.
statusToolCallMessagePartStatusstatus.type is "running", "requires-action", "complete", or "incomplete".
toolNamestringName of the tool the model called.
toolCallIdstringStable id for this invocation.
isErrorboolean | undefinedWhether result represents a tool execution error.

There is no dedicated plan-to-graph primitive: layout above is a plain function you write, turning whatever shape your tool's arguments take into the FlowNode[] and FlowEdge[] that FlowGraph expects. Register the renderer on a toolkit entry's render field and attach the toolkit with Tools({ toolkit }). See Tool UI for the full render-prop surface.