HITL is request-driven: the Assistant only pauses when its agent calls a HITL tool (
ask_user_* or request_credential). You do not configure interrupt points the way you do in the interrupt node — the agent decides when an answer or approval is needed.The pause/resume lifecycle
A turn that needs your input moves through a fixed lifecycle. The Assistant pauses, you answer, and a new run continues the same conversation.1
The turn starts
You send a message to
POST /assistant/chat. The turn begins streaming over the run’s SSE stream. See streaming responses.2
The Assistant asks
The agent calls a HITL tool. The run emits a
user_input_request SSE frame carrying the question, writes a pending audit row, sets a server-side pending sentinel, flips the run status to interrupted, and suspends. The stream stays open — there is no terminal frame.3
You answer
You call
POST /assistant/chat/{chat_id}/resume with the question’s request_id, a typed response, and your llm config. A three-layer guard validates the answer.4
A new run resumes
Resume mints a new
run_id, re-enters the same thread_id, and returns a fresh stream_url. You open that new stream to watch the turn finish.5
The turn completes
The continued run streams the rest of its work and ends with a
done frame (or pauses again on another question).Frame trace (one HITL turn)
Request kinds
A HITL question is aUserInputRequest, discriminated on its kind field. There are exactly five request kinds. The Assistant emits one inside the user_input_request SSE frame.
Common base fields
Every request kind shares these base fields.string
required
The unique id binding this question to its answer. You must echo it back on resume. Ids are prefixed by tool (for example
choice-, yesno-, text-, multi-, cred-). The id is UNIQUE on the audit row.string
required
The question text, rendered as Markdown.
boolean
default:"true"
Whether an answer is required. When
true, skipping is not offered.boolean
default:"false"
Whether the user may add free text alongside a structured answer (used by
single_choice).object
Optional structured context the agent attaches to the question. May be
null.integer
Optional advisory timeout for the UI. It is a hint only — the authoritative expiry is the pending sentinel’s 7-day TTL (see paused runs). May be
null.The five kinds
- single_choice
- multi_choice
- yes_no
- free_text
- credential_request
Pick one option from a list.Answer with a
ChoiceOption[]
required
1–10 options. Each
ChoiceOption is {value, label, description?, icon?, badge?}.single_choice response carrying selected_value (and optionally free_text when allow_free_text is true).Response kinds
You answer by sending aUserInputResponse, discriminated on its kind field. There are seven response kinds. Note the asymmetry: one request kind (credential_request) maps to two response kinds (credential_added / credential_failed), and skipped is a response-only kind with no matching request kind.
Response kind | Fields | Answers request kind |
|---|---|---|
single_choice | selected_value? (string), free_text? (string) | single_choice |
multi_choice | selected_values (string array) | multi_choice |
yes_no | answer (boolean) | yes_no |
free_text | text (string) | free_text |
credential_added | credential_id, integration_name, auth_type | credential_request |
credential_failed | integration_name, auth_type, error_code, error_message, retryable (default true), provider_details? | credential_request |
skipped | reason? (string) | any (response-only) |
enum
One of
oauth_denied, oauth_provider_error, invalid_credentials, network_error, popup_closed, timeout, unknown. Sending this kind hands the agent a structured failure so it can recover rather than commit to a broken credential.When you answer with
credential_added, the resume endpoint runs a server-side preflight: it tests the freshly added credential before the agent commits to it. If the test fails, the resume value is swapped to a credential_failed response with error_code: invalid_credentials and retryable: true, so the agent gets a clean failure instead of a broken credential. This is invisible to your code but can change the effective answer.Approval gates
An approval gate is the Assistant stopping for your confirmation before a sensitive or destructive action — for example, before sending an email or deleting a record. Mechanically, an approval is a HITL question (most often ayes_no request) that the agent raises ahead of the risky tool call; the action runs only after you answer affirmatively.
Stopping for approval before sensitive actions is a paid-plan capability (“manage approvals”), enforced by the Assistant’s tool-execution gating policy — distinct from the billing admission gate that meters each turn. Read-only actions generally run without a gate; write or destructive actions are gated. The set of gated actions is governed by ModuleX, not configured per chat.
Approval is not a separate request kind. It is a regular
yes_no (or single_choice) question with approval-shaped labels. Handle it like any other HITL question: stop reading the stream, present the choice, and resume with the user’s decision.Who can approve
Only the user who triggered the question may answer it — including approvals. A different user gets a
403. The retired member role cannot use the Assistant at all.Limits and entitlements
Which plans include managed approvals, and the usage limits that apply to Assistant turns.
Responding and resuming
You resume a paused chat by answering its open question. Resume mints a newrun_id; the original stream does not carry the continued events — you must open the new stream_url.
POST /assistant/chat/{chat_id}/resume
Authenticate every request with Authorization: Bearer mx_live_… plus X-Organization-ID, and the owner or admin role in that organization — see authentication. The caller must be the same user who triggered the question.
Request body
string
required
The paused question’s
request_id, taken from the user_input_request frame. It must match the current pending sentinel exactly.UserInputResponse
required
The typed answer, discriminated on
kind (one of the seven response kinds). A response that fails discriminated-union validation returns 422.object
required
The LLM selection used to rebuild the chat model on resume. Although the underlying field is schema-optional, the Assistant endpoint rejects a missing
llm with 400 — it is required in practice, because the graph persists state but not the in-process model. Keys: integration_name, provider_id, model_id, and optional credential_id.The three-layer resume guard
Every resume passes three checks before the run continues:1
Request match (read-only)
The pending sentinel must exist and
request_id must match it. A missing sentinel or mismatch returns 410 — the question was already answered, cancelled, or expired.2
Ownership (read-only)
The caller’s user id must equal the question’s originator. A different user returns
403.3
Atomic claim (compare-and-delete)
The pending sentinel is cleared with a
WATCH/MULTI/EXEC compare-and-delete. If two answers race, the loser returns 410.Response
string
resuming.string
The chat that was resumed.
string
A new
run_id, minted for the continued turn. Re-listen on this id.string
The conversation thread id. Equal to
chat_id and unchanged across the resume.string
The SSE URL for the new run:
/assistant/chat/{chat_id}/listen/{new_run_id}. Open it to watch the turn finish.assistant.resume requires llm (no default) in the Python and JavaScript SDKs, matching the endpoint’s 400-on-missing behavior. This differs from composer.resume, where llm defaults to None. See streaming and HITL.OAuth auto-resume
When acredential_request offers an oauth2 option and the user completes the OAuth flow, the chat resumes without a manual resume call. The OAuth callback re-runs the same three-layer guard, publishes a run_resumed event on the old run’s channel (so a live client swaps to the new stream), and continues the turn on a new run_id — funneling through the same resume path. This is why Assistant chats are stored alongside Composer chats: the shared storage keeps OAuth auto-resume working.
run_resumed on the OLD run's channel
run_resumed, stop reading the current stream and open /assistant/chat/{chat_id}/listen/{new_run_id}.
Resume errors
error
llm is missing from the resume body. Envelope: {"detail": "..."}.error
The caller is not the user who triggered the question. Envelope:
{"detail": "..."}. See roles and permissions.error
The chat does not exist or is not in your organization. Ownership failures return
404 (not 403) so existence is never leaked. Envelope: {"detail": "..."}.error
The question was already answered, cancelled, or expired, or your resume lost the atomic compare-and-delete race. Envelope:
{"detail": "..."}. There is no dedicated 410 exception class in the SDKs — it surfaces as the base error type.error
The
response failed discriminated-union validation (for example a missing required field for its kind). Standard FastAPI validation envelope.error
A billing or rate-limit denial on the continued turn. Flat
DenialEnvelope shape — see credit impact. Distinct from the 403 ownership error above, which uses the {"detail": "..."} shape.For the full pause/resume reference shared with the workflow-run variant — including how the interrupt node resumes via the workflow thread (reusing the same
run_id) rather than minting a new one — see human-in-the-loop (HITL) resume. The two flows are not interchangeable.Paused runs
A paused Assistant run is a turn suspended on an open HITL question. Understanding its server-side state explains the concurrency rules and the expiry.One pending question per chat
One pending question per chat
A chat may hold only one open question at a time. While a question is pending — or while a turn is still running —
POST /assistant/chat returns 409. The Assistant forces one tool call per model step, so two questions cannot fire in the same super-step. Answer or cancel the current question before starting a new turn.The pending sentinel (7-day TTL)
The pending sentinel (7-day TTL)
Each open question sets a server-side pending sentinel keyed to the chat, with a 7-day TTL. This is the authoritative window in which the question can be answered; the question’s
timeout_hint_seconds is only a UI hint. If the sentinel expires, a later resume returns 410.The run status
The run status
A paused run reports status
interrupted in the server-side run-status doc ({running, completed, failed, interrupted, cancelled}, 1-hour TTL). chat.running_id stays set so the chat→run binding is preserved. The status document is what GET /assistant/chat/{chat_id}/status reads to report awaiting_input and the pending_request_id.Rehydrating a pending question
Rehydrating a pending question
GET /assistant/chat/{chat_id} re-surfaces a pending question on refresh: when running_id is set, the pending sentinel exists, and the matching audit row’s outcome is still pending, the response includes pending_user_input_request with the full question payload. Otherwise it is null. This lets a client that disconnected pick the question back up without replaying the stream.Checking status without the stream
Checking status without the stream
GET /assistant/chat/{chat_id}/status returns awaiting_input, pending_request_id, is_running, running_id, and the raw run_status doc. is_running is true only when a run is active and not awaiting input — a paused run reports is_running: false, awaiting_input: true.Cancelling a paused run
POST /assistant/chat/{chat_id}/cancel cancels the active execution. If the run is paused on a HITL question, cancel also clears the pending sentinel and flips the audit row to cancelled, so GET /assistant/chat stops re-presenting the question. With no active run, cancel returns 400 ("No active execution to cancel").
Status of a paused, then cancelled, run
Credit impact
HITL does not change how a turn is metered — it shifts where the work happens.One run credit per turn
A turn is charged exactly one run credit when it starts at
POST /assistant/chat. A resume re-enters the conversation through POST .../resume, not /chat, so answering a question does not charge another run credit for the same turn.Token usage is metered separately
Language-model token usage is recorded on top of the run credit, on token counts, regardless of whether the turn succeeded, paused, or was cancelled. A paused turn that you resume keeps accumulating tokens on the continued run.
POST /assistant/chat and the resumed turn pass the synchronous billing admission gate before any work runs. On denial, the request is rejected before any rows are written and returns the flat DenialEnvelope:
DenialEnvelope (402 429)
layer maps to the status code: credit and wallet return 402, quota returns 403, and rate returns 429 (with Retry-After and X-RateLimit-* headers). This is distinct from the standard {"detail": "..."} envelope used by validation and ownership errors — see errors and status codes and rate limiting.
Reference: every HITL error
| Status | When | Envelope |
|---|---|---|
400 | resume body missing llm; or cancel with no active run | {"detail": "..."} |
403 | Caller is not the question’s originator; or not owner/admin | {"detail": "..."} |
404 | Chat not found or not in your organization (no existence leak) | {"detail": "..."} |
409 | POST /assistant/chat while a question is pending or a run is in progress | {"detail": "..."} |
410 | resume: question already answered, cancelled, expired, or lost the atomic claim | {"detail": "..."} |
422 | resume: response fails discriminated-union validation | FastAPI validation error |
402 / 403 / 429 | Billing or rate-limit denial on the turn or resume | DenialEnvelope {code, layer, ...} |
Where to go next
HITL resume reference
The full pause/resume reference, including the workflow-run variant and the shared question/answer wire contract.
Interrupt node (HITL)
The workflow node that pauses a run to ask a human a structured question — the builder counterpart to Assistant HITL.
Streaming responses
How to consume the Assistant’s SSE stream, including the
user_input_request and run_resumed frames.Using tools
How the Assistant discovers tools and requests credentials, the source of
credential_request questions.Permissions and limits
Who can use the Assistant and approve actions, and the usage limits that apply.
Streaming and HITL in the SDKs
The JavaScript and Python
resume methods, the nested-payload helper, and the new-run_id contract.