Skip to main content
A node is one step in a flow. You register it with .addNode(name, handler). The handler receives a context object and returns one of three things:
  1. A plain state update (action node).
  2. An interrupt signal (pause and ask).
  3. A widget signal (pause and delegate to a display tool).
The return type determines what the engine does next.
flow.addNode("ask_email", ({ interrupt }) =>
  interrupt({ email: { question: "What is your work email?" } }),
);

Node context

Every handler receives a single argument with this shape:
type NodeContext<TState> = {
  state: Partial<TState>;
  meta?: Record<string, unknown>;
  interrupt: TypedInterrupt<TState>;
  showWidget: TypedShowWidget<TState>;
  waniwani?: ScopedWaniWaniClient;
};
state
Partial<TState>
Current flow state. Partial because earlier nodes may not have filled every field yet.
meta
Record<string, unknown> | undefined
The MCP request _meta for this tool call. Rarely needed directly. Use waniwani for tracking.
interrupt
TypedInterrupt<TState>
Helper to build an interrupt signal. Field keys are typed against the state schema. See Interrupts.
showWidget
TypedShowWidget<TState>
Helper to build a widget signal. Accepts a RegisteredTool (from createTool()) or its id as a string.
waniwani
ScopedWaniWaniClient | undefined
A session-scoped tracking client. Present when the MCP server is wrapped with withWaniwani(). Undefined otherwise.

Action nodes

An action node returns a plain state update. The engine deep-merges the update into state and advances along the node’s outgoing edge without pausing.
.addNode("normalize_email", ({ state }) => {
  if (!state.email) return {};
  return { email: state.email.trim().toLowerCase() };
})
Action nodes are where you put transformations, API calls, database writes, and analytics. Multiple action nodes in a row all run inside one tool call. The engine only pauses when a node returns an interrupt or widget signal.
.addNode("check_stock", async ({ state, waniwani }) => {
  const inStock = await stockService.check(state.sku);
  await waniwani?.track({
    event: "tool.called",
    properties: { name: "check_stock", type: "other" },
  });
  return { inStock };
})

Interrupt nodes

An interrupt node returns interrupt(...). The engine stores the graph step, returns status: "interrupt" to the model, and waits for a continue call. When the user answers, the node’s validators run (if any) and the engine advances.
.addNode("ask_ski_level", ({ interrupt }) =>
  interrupt({
    level: {
      question: "What's your ski level?",
      suggestions: ["beginner", "intermediate", "advanced"],
    },
  }),
)
See Interrupts for multi-question interrupts, hidden context, validation, and re-ask on error.

Widget nodes

A widget node returns showWidget(tool, config). The engine returns status: "widget" pointing at a display tool (created with createTool()) and the flow pauses. When the user interacts, the host calls the flow back with action: "continue" and whatever values you want stored in stateUpdates.
.addNode("show_pricing", ({ state, showWidget }) =>
  showWidget(showPricingTool, {
    data: {
      postalCode: state.postalCode!,
      sqm: Number(state.squareMeters),
    },
    description: "User must pick a plan.",
    field: "selectedPlan",
  }),
)
Setting field enables the same auto-skip behavior as interrupts: if selectedPlan is already filled when the flow reaches this node, the widget is skipped. Setting interactive: false makes the widget display-only and auto-advances after it renders.
The flow itself is a data-only tool. It never returns structuredContent. The display tool referenced by showWidget is the one that renders the widget. Register both alongside each other with registerTools(server, [displayTool, flow]).

Async handlers

Handlers can be sync or async. Most real handlers are async because they touch external services:
.addNode("fetch_pricing", async ({ state }) => {
  const prices = await pricingApi.quote({
    postalCode: state.postalCode!,
    size: state.squareMeters!,
  });
  return { prices };
})

Naming and uniqueness

Node names are internal identifiers. They appear in Mermaid diagrams (flow.graph()), compile-time validation errors, and error messages at runtime. Use snake_case, keep them short, prefer verbs for actions and ask_* for interrupts. Node names must be unique within a flow. Adding the same name twice throws at build time. START and END are reserved and cannot be used as node names.