Skip to main content
A compiled flow registers itself as a single MCP tool. This page documents the wire contract between the model and that tool. You don’t usually parse it yourself, but it’s the reference if you’re building tracing, replay, or test harnesses.

Input

  • start begins a new run. intent is a short summary of why the user triggered the flow. The run starts from whatever its stateUpdates writes, so the flow skips the questions those values fill.
  • continue resumes the current run with the answers to the questions on the table, keyed by each question’s field, plus any other field from fields the user has stated and the model has not sent yet.
  • reset restarts from the beginning with a corrected value for a field the user already gave. Every other answer is kept.

What a call can write

The tool listing names no state field. stateUpdates is an untyped record, the same for every flow, so none of your field names, descriptions or enum values appear in tools/list. The model learns them from the responses. The session’s first interrupt or widget response lists every field the conversation may fill under fields, and each interrupt or widget response names the fields it asks for, with a fieldSchema (type, allowed values, description) taken from your state schema. Every call, start included, writes two kinds of value from stateUpdates:
  • The answer to a field the run has asked, as sent. The engine runs no schema check on it, since the question’s validator judges it.
  • Any other field the state declares, or member of a z.object group, whose value passes that field’s Zod schema. The flow skips the questions those values fill, and their validators run when the flow reaches the node.
A field no question asks, such as a classification the user’s opening message implies or a locale, can be set this way. A value that fails its schema is dropped, and so is a key the state does not declare. A reset with nothing it can write returns the error Nothing to correct: "reset" takes fields the flow listed in `fields` or has asked for. A field wrapped in neverPrefilled() is left out of fields and takes a value only once the run has asked it. Mark a consent or an attestation this way, along with a widget pick that must happen on screen. Every other state field is exposed to the conversation. A value the model must never set, such as a lookup id that decides what gets priced, stays out of state and lives in a store keyed by ctx.sessionId. See Values that stay on the server. The session record lists every field the run has asked under asked. An answer to one of those merges without a schema check, and a neverPrefilled field opens to writes once it is listed there.

Tool definition metadata

The tool definition’s _meta carries the flow graph (and the list of fields marked redacted()) for withWaniwani to read on the server. Both are wrapped so JSON serialization drops them: tools/list never sends them to a client.

Tool description

The registered tool’s description is the description you passed to createFlow, plus one sentence stating that the tool runs a multi-step flow and that each response describes the next call. Nothing else. Operating instructions stay out of it on purpose. A description that prescribes conversation behavior, tool sequencing, or required follow-up calls is what prompt-injection classifiers look for, and ChatGPT raises a Suspicious Instruction warning on the connector approval card when it finds one — the user is asked to approve a tool that looks like it is trying to steer the model. Write your description as a description of what the flow does, in one or two sentences. The protocol the assistant needs travels on the response instead, as nextStep.

Response statuses

Every tool call drives the engine until it hits one of four outcomes: Between 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 pause.

Response fields added by the engine

Four keys are attached by the engine rather than by a node: intro comes first, then the step’s own data, then fields, and nextStep last. On the session’s first question, a response looks like this:
When a response carries fields, nextStep opens with this line, after the intro line when there is one:
fields lists every field this flow fills from the conversation, keyed the way stateUpdates takes them. It appears once per conversation. Whenever the user states a value for a listed field, in any message, send it once in stateUpdates on your next call to this flow, matching its type and enum values. The flow fills those fields and skips the questions that would ask them.
nextStep is generated by the engine. It is not configurable, and a node cannot set it.

State persistence

State persists between calls in the flow store, keyed by the session id extracted from _meta. Default behavior is described in the Platform overview:
  • Without an explicit store and without WANIWANI_API_KEY, .compile() throws at compile time.
  • With an explicit store: KvStore, state lives wherever your adapter writes.
  • With WANIWANI_API_KEY and no explicit store, the SDK selects WaniwaniKvStore and state lives on app.waniwani.ai.
Alongside the flow’s own state, a session record carries step, the field being asked, the widget awaiting interaction, the fields the run has asked (asked), and an internal object of engine bookkeeping keyed per feature: an intro or fields listing still waiting to go out, dropped once delivered. A store adapter persists the record as given and never needs to interpret it. A write keeps what it loaded, so a field a newer version wrote survives a call served by an older one and a deploy mid-conversation does not strand a session. See State for the state schema side of the contract, and KV store adapters for backend options.