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). A latercontinueorresetcan write only these.
"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 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 skipped entirely. Values ofundefined, 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 sends that answer when the flow asks the question, and doesn’t ask the user again. If every question on the table is answered that way, the model resumes the flow right away. The interrupt response is where the model learns a field’s name and schema, and the engine accepts a value only for a field the run has asked for (see What a call can write). Group questions the user tends to answer together into one interrupt, so answering them from the conversation takes a single call.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.