Skip to main content

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.

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.
flow.addNode({
  id: "ask_email",
  run: ({ interrupt }) =>
    interrupt({ email: { question: "What is your work email?" } }),
});
An older positional form (addNode(id, run, options?)) is also accepted, but is deprecated. See Changelog / 0.12.0.

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({
  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.
.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.
.addNode({
  id: "ask_ski_level",
  run: ({ 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({
  id: "show_pricing",
  run: ({ state, showWidget }) =>
    showWidget("show_pricing", {
      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 the display tool with Skybridge’s 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 for the full pattern.

Async handlers

Handlers can be sync or async. Most real handlers are async because they touch external services:
.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.