Skip to main content
Human-in-the-loop (HITL) is how the Assistant pauses a turn to ask you something — a choice, a piece of text, a yes/no decision, an approval, or a missing credential — and then continues once you answer. While the Assistant is paused it holds exactly one open question, the run is suspended server-side, and no further action runs until you respond. This page covers every request and response kind, the approval gate, the resume contract, and what happens to a paused run. The Assistant inherits this machinery verbatim from the AI Composer; the wire contract is identical to the workflow interrupt node HITL question shape and to the SDK streaming and HITL flow. The full pause/resume reference, including the workflow-run variant, lives at human-in-the-loop (HITL) resume.
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)
A live HITL pause does not close the SSE stream and emits no terminal frame. The interrupted marker is written to history only — it is never published on the live channel. Do not wait for a terminal event after a user_input_request; treat the question itself as the signal to stop reading and answer. A later reconnect replays the history (including the interrupted marker) so the replay ends cleanly. See SSE run streaming.

Request kinds

A HITL question is a UserInputRequest, discriminated on its kind field. There are exactly five request kinds. The Assistant emits one inside the user_input_request SSE frame.
The question payload is nested one level deeper than every other run event. The SSE frame is {"type":"user_input_request","data":<UserInputRequest>}, so the question lives at data.data, not data. A naive read of frame.data.kind returns nothing — the kind is at frame.data.data.kind. The Python SDK ships user_input_request_from_event(event.data) to extract it for you.

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

Pick one option from a list.
ChoiceOption[]
required
1–10 options. Each ChoiceOption is {value, label, description?, icon?, badge?}.
Answer with a single_choice response carrying selected_value (and optionally free_text when allow_free_text is true).

Response kinds

You answer by sending a UserInputResponse, 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 kindFieldsAnswers request kind
single_choiceselected_value? (string), free_text? (string)single_choice
multi_choiceselected_values (string array)multi_choice
yes_noanswer (boolean)yes_no
free_texttext (string)free_text
credential_addedcredential_id, integration_name, auth_typecredential_request
credential_failedintegration_name, auth_type, error_code, error_message, retryable (default true), provider_details?credential_request
skippedreason? (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 a yes_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 new run_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 a credential_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
When you see 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.
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.
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.
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.
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.
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.
Both 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)
The 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.
The flat DenialEnvelope is emitted only on the metered surfaces — run, composer, assistant, and managed knowledge. Plain CRUD and org-settings routes return the {"detail": "..."} HTTPException shape instead. Your error handling on Assistant resume must account for both shapes.

Reference: every HITL error

StatusWhenEnvelope
400resume body missing llm; or cancel with no active run{"detail": "..."}
403Caller is not the question’s originator; or not owner/admin{"detail": "..."}
404Chat not found or not in your organization (no existence leak){"detail": "..."}
409POST /assistant/chat while a question is pending or a run is in progress{"detail": "..."}
410resume: question already answered, cancelled, expired, or lost the atomic claim{"detail": "..."}
422resume: response fails discriminated-union validationFastAPI validation error
402 / 403 / 429Billing or rate-limit denial on the turn or resumeDenialEnvelope {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.