> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Human in the Loop

> Approval gates, durable human requests, and signals that suspend a run to disk until a person answers — minutes or days later.

An agent is about to `terraform apply` against production. The orchestration script does the obvious thing: prints "Approve? (y/n)" and blocks on stdin. The reviewer is asleep. Eight hours later the laptop rebooted, the process is gone, and with it every upstream result the run had computed. The approval never happened; neither did the rollback.

That failure is not about UX. It is about where the wait lives. If "waiting for a human" is a blocked OS process, the wait is only as durable as that process — and humans operate on a timescale (hours, days, a weekend) that no process should be expected to survive. The only design that works when the human replies tomorrow is to persist the question as state, let the process exit, and reconstruct the run when the answer arrives.

Smithers treats every human interaction this way. An `<Approval>`, `<HumanTask>`, or `<WaitForEvent>` node writes a durable pending request, the run enters a wait status (`waiting-approval` or `waiting-event`), and the owning process exits. The decision is delivered later — CLI, MCP tool, gateway UI, or another agent — and the run resumes from persisted state as if nothing had stopped.

## How the industry solves it

Suspending on a human is the oldest problem in workflow engines. The mature systems all converged on the same shape: externalize the wait, correlate the reply.

| System                                                                                                                              | Mechanism                                                                                                                                                              | Tradeoff                                                                                                        |
| ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [Temporal](https://docs.temporal.io/develop/typescript/message-passing)                                                             | Signals delivered to a durable workflow; the handler updates state and `condition()` / `awaitConditions` unblocks the workflow code. Waits cost nothing while pending. | Full history-replay programming model; signals require the Temporal server cluster                              |
| [AWS Step Functions](https://docs.aws.amazon.com/step-functions/latest/dg/callback-task-sample-sqs.html)                            | `.waitForTaskToken`: a task emits a token, the state machine parks (up to a year), a human system calls `SendTaskSuccess` with the token                               | You build the approval UI and token plumbing yourself; AWS-only                                                 |
| [Azure Durable Functions](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview#human)         | `WaitForExternalEvent` with a timer race — the documented "human interaction" pattern escalates if the person never answers                                            | Same replay constraints as Temporal; Azure-only                                                                 |
| [Camunda / BPMN](https://docs.camunda.io/docs/components/modeler/bpmn/user-tasks/)                                                  | User Tasks: a first-class BPMN element with assignment, forms, and task lists. Twenty years of prior art from the business-process world                               | Heavyweight modeling; built for org-chart processes, not agent loops                                            |
| [LangGraph](https://docs.langchain.com/oss/python/langgraph/interrupts)                                                             | `interrupt()` throws, the graph checkpoints, and `Command(resume=...)` re-invokes the node from the checkpointer                                                       | Resume re-executes the interrupted node from its start; requires a configured checkpointer or the pause is lost |
| [CrewAI / AutoGen](https://microsoft.github.io/autogen/0.2/docs/tutorial/human-in-the-loop/)                                        | `human_input_mode="ALWAYS"` on a UserProxyAgent, or CrewAI's `human_input=True` — a console prompt in the live process                                                 | Blocks the process; no durability if the human walks away                                                       |
| [GitHub Actions](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environment-deployments) | Environment protection rules: required reviewers hold a deployment job until approval (30-day cap)                                                                     | Approval gates deployments only; the granularity is a job, not a step decision with a payload                   |
| [Argo CD](https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/)                                                           | Manual sync (auto-sync off) and sync windows as the promotion gate                                                                                                     | An approval by convention — no typed decision record, no per-step gates                                         |
| Claude Code / Cursor / Devin                                                                                                        | Interactive permission prompts per tool call (allow/deny/allowlist)                                                                                                    | Session-scoped; the pending prompt dies with the session                                                        |

The dividing line runs straight through this table: CrewAI, AutoGen, and coding-agent permission prompts **block a live process**; Temporal, Step Functions, Durable Functions, Camunda, and LangGraph **suspend durable state**. The first group works in a demo and fails on the first overnight wait. Smithers sits in the second group.

## How Smithers does it

Four surfaces, one mechanism. Each is a node in the workflow graph that persists a pending request and parks the run.

**`needsApproval` on a Task** is the minimal gate — pause before execution, no decision data:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
<Task id="deploy" output={outputs.deployResult} agent={deployer} needsApproval>
  Deploy to production.
</Task>
```

**`<Approval>`** is a decision node. It persists a typed `ApprovalDecision` row (modes: `approve`, `select`, `rank`) that downstream rendering branches on:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
<Approval
  id="ship-decision"
  output={outputs.shipDecision}
  request={{ title: "Ship release v1.4?", summary }}
  onDeny="continue"
  async
/>

{ctx.outputMaybe(outputs.shipDecision, { nodeId: "ship-decision" })?.approved
  ? <Task id="release" output={outputs.release} agent={releaser}>Ship it.</Task>
  : <Task id="rollback" output={outputs.rollback} agent={releaser}>Roll back.</Task>}
```

`onDeny` picks the denial policy (`"fail" | "continue" | "skip"`); `async` lets unrelated branches keep executing while the approval is pending; `autoApprove`, `allowedUsers`, and `allowedScopes` restrict who and what can auto-clear the gate. `<ApprovalGate when={...}>` is the conditional form: it demands a human only when `when` is true and otherwise emits a real `{ approved: true, note: "auto-approved" }` decision, so downstream branching stays uniform.

**`<HumanTask>`** collects arbitrary structured JSON validated against a Zod schema — the Camunda user-task shape, minus the BPMN. **`<Signal>` / `<WaitForEvent>`** wait for external events rather than decisions: `<Signal id="user-feedback" schema={outputs.feedback}>` renders `<WaitForEvent event="user-feedback">` internally, and `WaitForEvent` adds `correlationId` matching (the Step Functions task-token idea, but with a named event plus a key instead of an opaque token), `timeoutMs`, and `onTimeout: "fail" | "skip" | "continue"`.

Agents raise questions too, mid-task: the `ask-human` CLI command and the MCP `ask_human` tool create a durable pending human request from *inside* a running task and block that task until it is answered, cancelled, or expired — the escape hatch for "the agent is uncertain and must not guess."

### Suspension, not blocking

When a run hits an unresolved gate, `smithers up` **exits**. This is deliberate:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
bunx smithers-orchestrator up deploy.tsx        # exits with status waiting-approval
bunx smithers-orchestrator ps --status waiting-approval
bunx smithers-orchestrator approve RUN_ID --node ship-decision --by alice --note "LGTM"
bunx smithers-orchestrator up deploy.tsx --run-id RUN_ID --resume true
```

The pending request, the run frame, and every completed task output live in the database. Nothing is in memory; nothing is burning a process. When the human answers Tuesday, resume re-renders the graph, skips every completed task, and continues from the gate. The same flow drives signals — `smithers signal RUN_ID user-feedback --data '{"rating":5}'` then resume — and durable human requests: `smithers human inbox`, `smithers human answer <requestId> --value '"yes"'`.

<Note>
  Delivering a decision does not resume a suspended run by itself. An external controller — the Gateway's `submitApproval`/`submitSignal` + `resumeRun`, the `supervise` command, or an explicit `up --resume` — restarts execution. This is the same split Temporal makes between signal delivery and workflow progress, surfaced instead of hidden.
</Note>

### Operator surfaces

* **CLI:** `approve`, `deny` (auto-detect the node when only one gate is pending), `signal`, `human inbox|answer|cancel`, `ask-human`, `why RUN_ID` to see what a run is blocked on.
* **MCP:** `list_pending_approvals`, `resolve_approval` (with an ambiguity guard — it errors rather than guessing among multiple matches), `ask_human`. This is how an orchestrating agent (Claude Code, Hermes) relays a question to the human in conversation and records the answer.
* **Gateway UI:** the `ApprovalPanel` component from `@smithers-orchestrator/gateway-ui` renders pending approvals with approve/deny buttons over `useGatewayApprovals` + `submitApproval`; drop it into any [custom workflow UI](/guides/custom-workflow-ui).
* **Notifications:** the `@smithers-orchestrator/telegram` package plus the reference Telegram approval Mini App push gates to a phone; GitHub/Linear/Telegram integrations turn inbound webhooks into durable workflow signals.

## Guarantees and limits

**Guaranteed:** pending approvals, human requests, and awaited events survive process death, reboot, and days of silence — they are rows, not promises. Decisions are attributed (`decidedBy`, `note`, timestamp) and persisted as typed rows you can query later. Denials follow the declared policy instead of crashing the run. `resolve_approval` never guesses among ambiguous matches.

**Not guaranteed:** nobody is paged automatically — notification is your wiring (Telegram, a custom UI, an orchestrating agent watching `human inbox`). A delivered signal does not self-resume the run; something must call resume. `timeoutMs` on a gate is enforced when the run is being executed or resumed, not by a background clock while everything is parked. And unlike Camunda, there is no built-in assignment/escalation org model — `<EscalationChain>` composes one, but you define the levels.

## See also

* [`<Approval>`](/components/approval) · [`<ApprovalGate>`](/components/approval-gate) · [`<HumanTask>`](/components/human-task) · [`<Signal>`](/components/signal) · [`<WaitForEvent>`](/components/wait-for-event) · [`<EscalationChain>`](/components/escalation-chain)
* [Execution model](/concepts/execution-model) — wait statuses and resume semantics
* [MCP server](/integrations/mcp-server) — `ask_human`, `list_pending_approvals`, `resolve_approval`
* [Custom workflow UIs](/guides/custom-workflow-ui) — embedding `ApprovalPanel`
* [Telegram integration](/integrations/telegram) — approvals from a phone
