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.
"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.
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
If the user’s opening message provides an answer (passed viastateUpdates on action: "start"), the engine skips any interrupt whose fields are already populated. 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 of undefined, null, and "" do not count as filled.
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.