This page covers the state schema: the typed fields a flow collects. A flow also needs a place to persist that state between MCP calls. See Persistence, which is required for any flow to work.
State schema
Every flow declares state as a map of field names to Zod schemas. The runtime state type is inferred from this map, so ctx.state is fully typed inside every node. Each field’s description goes out in fields on the session’s first question or widget, and again with the question that asks for it, as its fieldSchema, so the model knows what it is collecting. The state schema itself never appears in the tool listing.
Inside a node:
Always add .describe() to every field. The model reads it when phrasing a question and when matching something the user said to one of the fields in fields. The most load-bearing piece of documentation your flow ships with.
State is always partial
At any point during execution, only the fields filled by earlier nodes are populated. The engine types ctx.state as Partial<TState>, so always guard when reading fields.
Field types
Prefer constrained types. The more constrained a field, the better the model fills it.
Enums pair well with suggestions
An interrupt that lists the same values as suggestions will lead the model to offer exactly those and validate the answer against the enum.
Nested state
Use z.object() when a sub-entity has multiple fields. Only one level of nesting is supported.
Interrupts target nested fields with dot-paths:
Action nodes that return nested objects are deep-merged, so returning { driver: { name: "John" } } does not wipe driver.licenseNumber.
driver.address.city will not work. Flatten deeper hierarchies or handle the sub-object in one question that fills the whole z.object() at once.
Answers in the user’s opening message
When the user’s opening message already contains answers (for example, “I’m in 75011 and we heat with gas, how much for my flat?”), the model sends them in stateUpdates on its next call. The session’s first question or widget lists the flow’s fields under fields, with their type, enum values and description, so the model can send values for questions the flow has not reached yet:
Both values pass their schemas, so the engine writes them and the flow skips the questions that would ask for them. A validator on one of those questions still runs when the flow reaches the node. A value that fails its field’s schema is dropped, and the flow asks the question as usual.
No extra configuration is needed. Write field descriptions clear enough for the model to map what the user said to the right field.
Any call writes a declared field whose value passes its schema, start included. That reaches fields no question asks, such as a classification the opening message implies or a locale. See What a call can write.
Fields only a question can fill
Wrap a field in neverPrefilled() when the model must not fill it ahead of its own question:
A marked field is left out of fields, and a value for it is dropped until the run has asked it, through an interrupt question, a widget’s field, or a question on its z.object group. Mark these:
- A consent or an attestation, which only counts as an answer to its own question.
- A widget pick that must happen on screen. A widget whose
field is already filled is skipped, so an unmarked widget field the model sent early skips the widget.
Apply neverPrefilled() at the end of the schema chain, like redacted(). The two combine on one field.
Values that stay on the server
Everything in state is exposed to the conversation: the model sees it in fields and can write it. A value the model must never see or set, such as the vehicle id a plate resolves to before it gets priced, does not belong in state.
Compute it in the node that uses it, or keep it in a store of your own, keyed by ctx.sessionId. That id is the session the flow’s own record is stored under, so the value lives exactly as long as the conversation:
A computed field can stay in state when the node that writes it runs on every pass and never skips because the field is already set. Its value then always comes from the node. A guard such as if (state.vehicleId) return {}; keeps whatever the model sent.
How state updates happen
Nodes update state in three places:
- Action nodes return a plain object that is deep-merged into state.
- Interrupts store the user’s answer at the field key used in
interrupt({ field: { question } }).
- Interrupt validators (see Interrupts) can return additional state updates after a user answer passes validation.
All three paths are typed against the state schema.