Skip to main content
suspend() pauses a run from inside a handler and waits for an externally-supplied value. It’s the general form of the approval gate (an approval resumes with a bool, a suspension resumes with anything) and it’s how you build tools like ask_user, confirm, or pick_option that let an agent explicitly stop and hand control back to the user mid-loop.

How it works

  1. A handler calls suspend(payload, kind=...).
  2. The run ends with status cancelled / reason input_required and emits an InteractionEvent carrying payload.
  3. The frontend renders the payload (keyed by kind) and collects a response.
  4. You resume by calling the runnable again with the original run as parent_id and resume={interaction_id: value}. The handler re-executes and suspend() returns value.

Writing an interaction tool

A tool is any handler that calls suspend():
suspend is exported at the top level (from timbal import suspend), and the ready-made ask_user / ask_user_multi / confirm tools ship in timbal.tools (from timbal.tools import ask_user, ask_user_multi, confirm).
Because the handler re-executes from the top on resume, suspend() must come before any non-idempotent side-effect in the handler (same constraint as LangGraph’s interrupt()). Put the suspend() call first, or make everything before it idempotent. This is exactly why irreversible actions belong behind an approval gate instead: the gate guarantees zero handler code runs until approved.

InteractionEvent

The interaction_id is a deterministic hash of the handler path + payload, stable across the pause/resume round-trip, so the client always sends back the id it received. tool_call_id lets the client correlate the interaction with the exact tool_use block in the chat transcript when the agent triggered it.

Declaring a response schema

Pass response_schema to suspend() to tell the client what shape the resume value must be. It rides on the InteractionEvent so the frontend can validate the user’s input before resuming, with no extra round-trip to discover a bad value.
The schema is advisory metadata for the client. None (the default) means any value is accepted.
response_schema is not enforced server-side. Timbal hands the resume value to the handler verbatim and does not validate it against the schema. Treat the schema as a contract for your client to validate against before resuming; if you need server-side guarantees, validate inside the handler after suspend() returns.

See also