Human-in-the-Loop
A human-in-the-loop (HITL) step pauses an agent run, asks one or more humans to make a decision, and resumes the run with their choice as the step's output. Downstream control-flow steps (if_else / switch / gate) route on the chosen outcome — the HITL step itself never branches.
This page is the conceptual overview; for the field reference and configuration details, see the Human-in-the-Loop Step reference.
When to use HITL
Reach for HITL when an agent shouldn't proceed without a human sanity-check. Common patterns:
- Approval gates before side effects. The agent drafts an outbound email, a content publish, or a webhook payload; a person reviews and approves before it leaves the building.
- Change-management with M-of-N quorum. A production deployment, a policy edit, or a credit refund needs N people to sign off, not just one.
- Conditional escalation on low confidence. Pair an
evaluate_step(LLM-as-judge scoring) with anif_elseso high-confidence outputs auto-approve and only borderline ones park for a human. See the HITL use-case examples for the full conditional pattern. - Triage / escalation. The agent classifies an incoming message as "unsure" and asks a human which bucket it belongs in.
- Sensitive customer interactions. A complaint or refund request is routed to a human before the agent responds.
- Compliance audits. A workflow needs a recorded human signature alongside the automated decision for regulatory traceability.
HITL is the wrong tool when you want the agent to ask the user a follow-up question as part of a conversation — that's a dialog turn, not an approval gate. Use a conversational front-end (chat / dialog flow) instead.
Lifecycle: park, decide, resume
A HITL step has three runtime phases:
- Park. When the agent reaches the step, the runner snapshots the recipient list, renders the prompt against upstream step outputs, persists an
AgentHitlRequest, and dispatches signed deep-link emails. The agent run and the step run both transition to awaiting_humanstatus and the orchestrator stops scheduling downstream work. - Decide. Recipients click the email link (or open the Approvals inbox at
/app/<account>/hitl) and submit a vote with an optional comment. Each vote is recorded; whenrequired_approvalsvotes for the same choice arrive, the request resolves. - Resume. The aggregate result — the winning outcome plus the full vote ledger — is written as the step's output, the run transitions back to
processing, and the orchestrator dispatches the HITL step'schild_steps. Downstream gates (typically anif_elseon{{step.<hitl_id>.output.outcome}}) route on the decision.
Parking is durable — the agent run can sit in waiting_human for minutes, hours, or days without consuming runtime credits or holding a worker. The passive wait time is excluded from agent runtime everywhere we report duration (run details, duration-stats, daily aggregates), so HITL-heavy agents don't look slow.
Who gets notified
The recipient_distribution field picks the audience. On a personal account, the only available recipient is the account owner — the agent editor hides the selector and just confirms that the owner is notified. On an organization account, three modes are available:
owner— only the account owner.owner_admins— owner plus all organization administrators. This is the default.selected_members— a hand-picked list (the editor shows a member-picker populated from org membership).
Recipients are resolved at park time, not at config time. If an admin is added to the org after a request was parked, they will not receive that request — the snapshot is frozen on the original recipient list.
Each recipient can tune their own email noise floor at /app/settings/email: a hard cooldown suppresses further HITL emails for N minutes after one is sent. Suppressed recipients still see the request in their inbox — only the email is dropped.
M-of-N quorum
The required_approvals field (the M in M-of-N) controls how many matching votes resolve the request:
required_approvals: 1— first responder wins. The first vote (any choice) resolves the request. This is the default and the right answer for most flows.required_approvals: N (N > 1)— M-of-N change management. The first choice to reach M votes wins. If every recipient votes and no choice reaches M, the request resolves withno_quorumas the outcome.
The agent editor enforces an upper bound on M based on the recipient pool size (the selected-members list length, or the org admin count for owner_admins) so you can't configure a quorum that's mathematically unreachable. Removing a member from a selected_members list auto-shrinks M to stay valid.
Approve/deny vs custom choices
By default a HITL step offers ["approve", "deny"]. Authors can replace this with a custom labeled list — for example ["ship_it", "needs_revision", "abandon"] for a multi-way release gate. Downstream gates dispatch on the chosen label.
A handful of labels are reserved as sentinel outcomes and may not appear in choices:
timeout— emitted when the request expires.no_quorum— emitted when every recipient voted but no choice reached M.cancelled— emitted when an operator cancels the request via the inbox or the MCP tool.
The output object always carries the winning outcome plus a votes array recording every vote (user, choice, comment, timestamp) — useful for audit logs and downstream routing that wants to read the comments.
Timeouts and reminders
A HITL request can wait indefinitely (no timeout_seconds set) or expire after a configured number of seconds. When the timeout fires, two outcomes are possible:
emit_timeout(default) — the step output is{"outcome": "__timeout__", ...}and the run continues into the downstreamchild_steps. This is the right choice when timeouts are routine — fall through to a "no human responded, do X" branch.fail_run— the run transitions to FAILED with a timeout error. This is the right choice when missing the deadline is itself a failure that should surface in the agent's error metrics.
If reminders are configured (reminder_interval_seconds + reminder_max_count), the janitor sends follow-up emails to recipients who haven't voted, up to the cap. Reminders are only available when a timeout is set — we never remind indefinitely.
Operating the inbox
Pending requests live under /app/<account>/hitl (linked from the Human in the Loop entry in the left nav under Observe). The inbox supports:
- Status filtering (pending / decided / expired / cancelled).
- A "waiting on me" toggle that restricts the list to requests where the current user is on the recipient snapshot.
- An Agents multi-select that filters the list to one or more specific agents — populated from agents that have a HITL step in their current definition AND agents with any historical HITL requests (so deleted-step backlog stays reachable).
- A time-window selector for narrowing the view to a specific period; the selection persists across the Overview and Review tabs.
- Inline cancellation of a pending request by the agent owner — the cancel emits the
__cancelled__sentinel as the step output so downstream gates can route the cancellation explicitly. Cancellation accepts an optional reason that the inbox surfaces alongside the resolved outcome.
The Overview tab shows a stacked daily bar chart broken down by status (pending / decided / expired / cancelled) and surfaces an "Approvals waiting on me" alert when the calling user is on the recipient snapshot for any pending request.
Each request also has a dedicated review page that shows the rendered prompt, the choice list, the votes received so far, the count of recipients who haven't voted yet (the "awaiting" set), and a vote-submission form. Email recipients land on this page directly via the signed deep link.
Billing
HITL steps are billed per outgoing email notification, at the same rate as send_email steps. Recipients in per-user email cooldown cost nothing — the dispatcher silently drops their email and the credit usage is calculated from the count of emails actually scheduled.
There is no runtime charge while the run is parked. The wall-clock wait between park and resume is excluded from agent runtime in every duration metric (the run-detail page, the per-run duration-stats chart, the daily aggregates that drive the dashboard). HITL-heavy agents don't look slow and don't bleed credits while waiting.
MCP and API
HITL is fully scriptable. Four MCP tools are available:
list_pending_hitl_requests— list requests, filterable by status, agent, orwaiting_on_me. Returns a paginated array including agent metadata, the vote ledger, and the resolved outcome.get_hitl_request— read a single request by ID. Useful for polling resolution (call again every few seconds untilstatus != "pending") without paginating the inbox.vote_on_hitl_request— submit the calling user's vote. The parked run resumes automatically once the quorum is met.cancel_hitl_request— cancel a pending request and emit thecancelledoutcome. The optionalreasonargument is recorded on the request's metadata and surfaced ascancellation_reasonin the step's output payload and on the inbox detail page.
REST endpoints mirror the MCP tools under /authenticated/hitl/.... A public signed-token vote endpoint (POST /hitl/{request_id}/vote) backs the email deep links so recipients can vote without an active session. All endpoints carry full OpenAPI metadata (summary, description, request/response models) — third-party integrators can build a custom HITL UI directly on top of these endpoints.
Related reading
- Human-in-the-Loop Step reference — field-level documentation and configuration examples.
- Control Flow Steps — sibling control-flow primitives (gate, if/else, switch) used to route on the HITL outcome.
- Agent Steps Overview — how steps compose into agent workflows.
- Credits & Usage — broader credit-rate documentation.