Skip to main content
suspend() lets a tool pause the run and hand control to the user, then resume with whatever they send back. Each tool picks a kind (the frontend’s renderer discriminator) and a payload (what to render). The resume value can be any JSON type — a string, a bool, a list, a dict — so the same primitive covers everything from a yes/no to a multi-field form. See the full reference in the Human in the Loop section. This page is a catalog of shapes you can copy.

A catalog of interaction tools

suspend is exported at the top level (from timbal import suspend). Ready-made ask_user, ask_user_multi, and confirm also ship in timbal.tools (from timbal.tools import ask_user, ask_user_multi, confirm).
The handler re-executes from the top on resume, so put suspend() before any non-idempotent side-effect (or make everything before it idempotent). For irreversible actions, gate them with requires_approval instead — that pauses before any handler code runs.

Payload shapes at a glance

This is the contract your frontend renders against. Switch your UI on kind, render payload, and send the matching resume value back keyed by interaction_id.

Driving the loop

Give an agent whichever interaction tools fit your product, then run the pause/resume loop. The agent decides which tool to call; you render the payload and resume with the value.
A single turn can open multiple interactions at once (the model calls several tools in parallel). You receive one InteractionEvent per question and send all answers back in one resume map: { "<id1>": ..., "<id2>": ... }. Approval gates ride the same channel — mix freely.

Structured form example

ask_form is the workhorse for collecting several values in one round-trip instead of a chain of questions:
The emitted InteractionEvent.payload is exactly the dict you passed; resume with the filled values:

Generative UI: render, then continue

review_chart shows how the same mechanism powers “render something, let the user tweak it, then keep going”. The tool emits the data to draw; the resume value carries the user’s edits back into the run:
If the user changes the range, you get {"approved": True, "edits": {"range": "90d"}} back and the handler continues with their choice — no separate “apply” endpoint needed.

Resuming across processes

Everything above works in-process. To pause in a browser now and resume in a worker later, configure a durable provider and pass parent_id on resume:
See Resuming a paused run and Client integration (HTTP) for the durable providers and the HTTP /stream wire contract your frontend talks to.