interrupt(...), the engine returns status: "interrupt" to the model, persists the current step, and waits. On the next action: "continue" call, the user’s answers arrive in stateUpdates, validators run, and the engine advances along the node’s outgoing edge.
What gets persisted
Between an interrupt and the matching continue call, the flow store holds:- The current step name (which node is paused).
- The full state snapshot at the time of pause.
- For single-question interrupts, the field being asked.
- Every field the run has asked so far (
asked). An answer to one of these merges as sent, and a field markedneverPrefilled()takes a value only once it is listed here.
"nodeName:fieldName". They are not serialized.
This has one operational consequence: if the server process restarts between the interrupt and the continue call, persisted state is reloaded but validator functions are rebuilt from the handler’s return value as the interrupt re-executes. The interrupt handler must therefore be deterministic enough to produce the same validator for a given state.
Single question
The smallest interrupt asks one question that writes its answer to one field.email) must be a field in the flow’s state schema. TypeScript enforces this.
Suggestions
Passsuggestions to give the model a concrete list to offer. Pairs well with z.enum() state fields so the model validates the user’s choice against a fixed set.
"flow" suggestion origin in:
<WaniwaniChat>:overrides={{ suggestionOrigins: ["channel", "page", "flow", "followup"] }}<script>embed:data-suggestion-origins="channel,page,flow,followup"<ChatEmbed>(bare primitive):suggestions={{ origins: ["flow"] }}
Hidden context
Sometimes the question you show the user differs from the guidance you want to give the model. Put the guidance incontext. It is passed through the protocol marked as hidden, so the model uses it to shape phrasing without showing it verbatim.
Per-question context lives inside the question config:
Multi-question interrupts
A single interrupt can ask several questions at once. The model asks them in one conversational message and the user’s answers arrive together.Validation and re-ask
Add avalidate function to reject bad answers or enrich state.
value is typed from the Zod schema for that field. The function can:
- Return
voidto accept the answer as-is. - Return a
Partial<TState>object to merge extra updates (for example, an enriched lookup result). - Throw an
Error(not return a string) to reject the answer.
- Clears the offending field from state.
- Re-presents the same interrupt.
- Prepends
ERROR: <message>to that specific question’scontextso the model can relay the reason naturally.
Nested fields
If state usesz.object() for one level of nesting, target sub-fields with dot-paths.
Auto-skip when a field is already filled
The engine skips any interrupt whose fields are already populated, whether an earlier answer, a value the model sent before the question came up, a validator or an action node filled them. For multi-question interrupts, already-answered questions are filtered out and only the unanswered ones are presented. An interrupt whose fields are all filled is never shown: its validators run on those values, and the flow moves on unless one of them throws. Leave that filtering to the engine. A node that drops a question once its field is set (...(!state.breed ? { breed: { validate } } : {})) also drops its validator, so a value the model sent early never gets checked.
Values of undefined, null, and "" do not count as filled.
Answers the user already gave
When the user’s opening message already answers a question (“I’m in 75011, quote me a half-day lesson”), the model doesn’t ask the user again. The session’s first interrupt or widget lists every field the conversation may fill underfields, so the model sends each value the user has stated on its next call, even for a question the flow has not reached yet. If every question on the table is answered that way, the model resumes the flow right away.
A value sent ahead of its question is written when it passes the field’s Zod schema, and the flow skips that question when it gets there. A field marked neverPrefilled() waits for its own question. See What a call can write.
Gotchas
- Throw, do not return strings. A validator that returns a string is treated as
void, so the answer is accepted. Thrownew Error("...")to reject. - Validators run on all-filled, not per-question. In a multi-question interrupt, a validator for question A will not fire until question B is also answered. Design validations accordingly.
- Validator closures can reference
statefrom the handler. Thatstateis the snapshot at the time the handler ran, not at the time the user answered. If you need the freshest state, do the lookup inside the validator using the value argument plus your own fetch, not the closed-overstate. - The field is cleared on error, not the whole interrupt. After a failed validation, only the failing field is empty. The user’s other answers remain.