# 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 (<code>if_else</code> / <code>switch</code> / <code>gate</code>) 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](https://seclai.com/docs/agent-steps/control#human-in-the-loop-step).

---

## 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`](https://seclai.com/docs/agent-steps/core#evaluate-step) (LLM-as-judge scoring) with an `if_else` so high-confidence outputs auto-approve and only borderline ones park for a human. See the [HITL use-case examples](https://seclai.com/docs/agent-steps/control#hitl-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:

1. **Park.** When the agent reaches the step, the runner snapshots the recipient list, renders the prompt against upstream step outputs, persists an <code>AgentHitlRequest</code>, and dispatches signed deep-link emails. The agent run and the step run both transition to a <code>waiting_human</code> status and the orchestrator stops scheduling downstream work.
2. **Decide.** Recipients click the email link (or open the **Approvals** inbox at <code>/app/&lt;account&gt;/hitl</code>) and submit a vote with an optional comment. Each vote is recorded; when <code>required_approvals</code> votes for the same choice arrive, the request resolves.
3. **Resume.** The aggregate result — the winning outcome plus the full vote ledger — is written as the step's output, the run transitions back to <code>processing</code>, and the orchestrator dispatches the HITL step's <code>child_steps</code>. Downstream gates (typically an <code>if_else</code> on <code>{`{{step.<hitl_id>.output.outcome}}`}</code>) route on the decision.

Parking is durable — the agent run can sit in <code>waiting_human</code> 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 <code>recipient_distribution</code> 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:

- <code>owner</code> — only the account owner.
- <code>owner_admins</code> — owner plus all organization administrators. This
  is the default.
- <code>selected_members</code> — 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 <code>/app/settings/email</code>: 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 <code>required_approvals</code> field (the M in M-of-N) controls how many matching votes resolve the request:

- <code>required_approvals: 1</code> — **first responder wins.** The first vote
  (any choice) resolves the request. This is the default and the right answer
  for most flows.
- <code>required_approvals: N (N &gt; 1)</code> — **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 with <code>__no_quorum__</code> as 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 <code>owner_admins</code>) so you can't configure a quorum that's mathematically unreachable. Removing a member from a <code>selected_members</code> list auto-shrinks M to stay valid.

---

## Approve/deny vs custom choices

By default a HITL step offers <code>["approve", "deny"]</code>. Authors can replace this with a custom labeled list — for example <code>["ship_it", "needs_revision", "abandon"]</code> 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 <code>choices</code>:

- <code>__timeout__</code> — emitted when the request expires.
- <code>__no_quorum__</code> — emitted when every recipient voted but no choice
  reached M.
- <code>__cancelled__</code> — emitted when an operator cancels the request via
  the inbox or the MCP tool.

The output object always carries the winning <code>outcome</code> plus a <code>votes</code> 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 <code>timeout_seconds</code> set) or expire after a configured number of seconds. When the timeout fires, two outcomes are possible:

- <code>emit_timeout</code> (default) — the step output is
  <code>{`{"outcome": "__timeout__", ...}`}</code> and the run continues into
  the downstream <code>child_steps</code>. This is the right choice when
  timeouts are routine — fall through to a "no human responded, do X" branch.
- <code>fail_run</code> — 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 (<code>reminder_interval_seconds</code> + <code>reminder_max_count</code>), 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 <code>/app/&lt;account&gt;/hitl</code> (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 <code>\_\_cancelled\_\_</code> 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 <code>send_email</code> 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:

- <code>list_pending_hitl_requests</code> — list requests, filterable by status,
  agent, or <code>waiting_on_me</code>. Returns a paginated array including
  agent metadata, the vote ledger, and the resolved outcome.
- <code>get_hitl_request</code> — read a single request by ID. Useful for
  polling resolution (call again every few seconds until
  <code>status != "pending"</code>) without paginating the inbox.
- <code>vote_on_hitl_request</code> — submit the calling user's vote. The parked
  run resumes automatically once the quorum is met.
- <code>cancel_hitl_request</code> — cancel a pending request and emit the
  <code>__cancelled__</code> outcome. The optional <code>reason</code> argument
  is recorded on the request's metadata and surfaced as
  <code>cancellation_reason</code> in the step's output payload and on the inbox
  detail page.

REST endpoints mirror the MCP tools under <code>/authenticated/hitl/...</code>. A public signed-token vote endpoint (<code>POST /hitl/{`{request_id}`}/vote</code>) 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](https://seclai.com/docs/agent-steps/control#human-in-the-loop-step) — field-level documentation and configuration examples.
- [Control Flow Steps](https://seclai.com/docs/agent-steps/control) — sibling control-flow primitives (gate, if/else, switch) used to route on the HITL outcome.
- [Agent Steps Overview](https://seclai.com/docs/agent-steps) — how steps compose into agent workflows.
- [Credits & Usage](https://seclai.com/docs/credits-usage) — broader credit-rate documentation.
