> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Nodes

> Handlers that advance the flow: action, interrupt, or widget.

A node is one step in a flow. You register it with `.addNode({ id, run, label?, hideFromFunnel? })`. The `run` 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.

```ts theme={null}
flow.addNode({
  id: "ask_email",
  run: ({ interrupt }) =>
    interrupt({ email: { question: "What is your work email?" } }),
});
```

<Tip>
  An older positional form (`addNode(id, run, options?)`) is also accepted, but is deprecated. See [Changelog / 0.12.0](/sdk/changelog#0-12-0-addnode-object-form).
</Tip>

## Node context

Every handler receives a single argument with this shape:

```ts theme={null}
type NodeContext<TState> = {
  state: Partial<TState>;
  meta?: Record<string, unknown>;
  interrupt: TypedInterrupt<TState>;
  showWidget: TypedShowWidget<TState>;
  waniwani?: ScopedWaniWaniClient;
};
```

<ResponseField name="state" type="Partial<TState>">
  Current flow state. Partial because earlier nodes may not have filled every field yet.
</ResponseField>

<ResponseField name="meta" type="Record<string, unknown> | undefined">
  The MCP request `_meta` for this tool call. Rarely needed directly. Use `waniwani` for tracking.
</ResponseField>

<ResponseField name="interrupt" type="TypedInterrupt<TState>">
  Helper to build an interrupt signal. Field keys are typed against the state schema. See [Interrupts](/sdk/flows/interrupts).
</ResponseField>

<ResponseField name="showWidget" type="TypedShowWidget<TState>">
  Helper to build a widget signal. Accepts a `RegisteredTool` (from `createTool()`) or its id as a string.
</ResponseField>

<ResponseField name="waniwani" type="ScopedWaniWaniClient | undefined">
  A session-scoped tracking client. Present when the MCP server is wrapped with [`withWaniwani()`](/sdk/configuration/wrap-server). Undefined otherwise.
</ResponseField>

## 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.

```ts theme={null}
.addNode({
  id: "normalize_email",
  run: ({ 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.

```ts theme={null}
.addNode({
  id: "check_stock",
  run: 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.

```ts theme={null}
.addNode({
  id: "ask_ski_level",
  run: ({ interrupt }) =>
    interrupt({
      level: {
        question: "What's your ski level?",
        suggestions: ["beginner", "intermediate", "advanced"],
      },
    }),
})
```

See [Interrupts](/sdk/flows/interrupts) for multi-question interrupts, hidden context, validation, and re-ask on error.

## Widget nodes

A widget node returns `showWidget({ tool, ... })`. 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`.

```ts theme={null}
.addNode({
  id: "show_pricing",
  run: ({ state, showWidget }) =>
    showWidget({
      tool: "show_pricing",
      data: {
        postalCode: state.postalCode!,
        sqm: Number(state.squareMeters),
      },
      field: "selectedPlan",
    }),
})
```

<Note>
  The positional form `showWidget(tool, config)` is **deprecated** but still supported for back-compat. Migrate to the object form for new code.
</Note>

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.

<Note>
  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 the display tool with [Skybridge's](https://docs.skybridge.tech) `server.registerWidget(name, meta, schema, handler)` and the flow with `flow.register(server)` — Skybridge handles all the widget-binding metadata for you. See the [Pet insurance example](/sdk/guides/pet-insurance#register-the-widget-display-tools) for the full pattern.
</Note>

## Async handlers

Handlers can be sync or async. Most real handlers are async because they touch external services:

```ts theme={null}
.addNode({
  id: "fetch_pricing",
  run: 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.
