Skip to main content
A flow is a graph of nodes that compiles into a single MCP tool. Each tool call runs the engine one step at a time: it asks a question, validates the answer, branches on state, then persists progress server-side under the MCP session id. The model never has to carry a token between calls. Flows exist because MCP tools are stateless. Anything longer than a single-shot Q&A (onboarding, quoting, qualification, triage) otherwise becomes a hand-rolled state machine serialized through the model.

What you declare

  1. State. The fields the flow collects, as a map of Zod schemas. State is type-inferred.
  2. Nodes. Handlers that return state updates, an interrupt signal, or a widget signal.
  3. Edges. How nodes connect. Direct or conditional.
You call .compile() on the graph to get a RegisteredFlow, then register it on your MCP server.

Minimal flow

import { createFlow, END, START } from "@waniwani/sdk/mcp";
import { z } from "zod";

export const onboardingFlow = createFlow({
  id: "onboarding",
  title: "User Onboarding",
  description: "Use when a new user wants to get started.",
  state: {
    email: z.string().describe("Work email"),
    useCase: z.string().describe("What they want to build"),
  },
})
  .addNode("ask_email", ({ interrupt }) =>
    interrupt({ email: { question: "What is your work email?" } }),
  )
  .addNode("ask_use_case", ({ interrupt }) =>
    interrupt({
      useCase: {
        question: "What do you want to build?",
        suggestions: ["Analytics", "Support", "Lead capture"],
      },
    }),
  )
  .addEdge(START, "ask_email")
  .addEdge("ask_email", "ask_use_case")
  .addEdge("ask_use_case", END)
  .compile();
Register it on your server:
await onboardingFlow.register(server);
That is a complete, working flow. See Register for full server wiring.

Execution model

Every tool call drives the engine until it hits one of three outcomes:
  • Interrupt. Pause and ask the user one or more questions. The response carries status: "interrupt".
  • Widget. Pause and delegate rendering to a display tool. The response carries status: "widget".
  • Complete. The graph reached END. The response carries status: "complete" and server-side state is deleted.
Between those pause points, the engine auto-advances through action nodes (nodes that return a plain state update). No round-trip to the model. A single tool call can run several action nodes before hitting the next interrupt. State persists between calls in the flow store, keyed by the session id extracted from _meta. The default store is WaniwaniFlowStore, backed by the WaniWani API (reads WANIWANI_API_KEY and WANIWANI_API_URL from env). It works out of the box in serverless runtimes.

Tool contract

A compiled flow exposes a tool with this input shape:
type FlowToolInput = {
  action: "start" | "continue";
  intent?: string; // required when action is "start"
  stateUpdates?: Record<string, unknown>;
};
  • start begins a new run. intent is a short summary of why the user triggered the flow. stateUpdates can pre-fill any known fields so the engine auto-skips those questions.
  • continue resumes the current run with the user’s latest answers in stateUpdates.
Responses are JSON with status: "interrupt" | "widget" | "complete" | "error". The protocol is documented inside the generated tool description, so the model knows how to call it. You do not parse responses yourself in normal usage.

State

Declare the fields the flow collects. Type inference flows from here.

Nodes

The three kinds of nodes and the context they receive.

Edges

Direct edges, conditional edges, loops, and compile-time validation.

Interrupts

Pausing, resuming, validation, and re-ask on error.

Register

Wire the compiled flow onto an MCP server end-to-end.