Simple Gates: needsApproval
Start with the simplest thing that could work. If you just need a human to say “go” before a task runs, put needsApproval on it:
needsApproval when you need a checkpoint, not a choice. “Should we proceed?” is a gate. “What should we do next?” is not.
Explicit Nodes: <Approval>
Here’s where it gets interesting. What if a denial isn’t an error — it’s information? What if “no” means “take a different path” rather than “stop everything”?
That’s the aha moment: an approval can be data, not just a gate.
When downstream tasks need to read the decision and branch on it, use <Approval>:
onDeny="continue" and the branching logic that follows. A denial doesn’t kill the workflow — it flows through as a value. The downstream task reads decision.approved and acts accordingly.
The <Approval> node produces an ApprovalDecision:
Denial Policies
So a human said “no.” Now what? TheonDeny prop answers that question, and the right answer depends entirely on what you’re protecting.
onDeny="fail" (default)
The workflow fails. Full stop. Use this for actions where denial means “this should not have been attempted”:
onDeny="continue"
The denial resolves as a decision value and the workflow keeps going. This is the policy that turns approvals into branching logic:
onDeny="skip"
The protected branch is skipped, but the rest of the workflow continues as if the approval node were never there:
Choosing a Denial Policy
| Policy | Use when… |
|---|---|
"fail" | Denial should stop the entire workflow (deploys, releases) |
"continue" | You need to branch on the decision (publish vs reject) |
"skip" | The approved work is optional and the workflow should continue without it |
"fail". It’s the safest default, and you can always loosen it later. You cannot un-deploy to production.
The Approval Lifecycle
What actually happens when the workflow hits an approval node? Here’s the full lifecycle, and every state is durable:- Smithers reaches the approval node
- It persists an approval request record (title, summary, metadata)
- The workflow suspends in a durable waiting state
- A human approves or denies via CLI, API, or UI
- The node resolves according to its denial policy
- Downstream tasks become eligible to run
Multi-Step Approvals
Real pipelines often need more than one checkpoint. Each<Approval> is independent — they don’t know about each other, and they don’t need to:
Approval with Context
A bare “Deploy to production?” is not very helpful when you’re the one being asked at 11 PM. Give your approvers what they need to decide:title and summary are what the human sees. The metadata is persisted alongside the request and available in smithers inspect, so the approver can check the commit, review which files changed, and make an informed decision without switching tools.
Good approval context is the difference between “I guess?” and “Yes, ship it.” Invest in it.
needsApproval vs <Approval>: When to Use Each
| Feature | needsApproval | <Approval> |
|---|---|---|
| Produces a decision value | No | Yes (ApprovalDecision) |
| Downstream branching | Not possible | Branch on decision.approved |
| Denial policies | Implicit fail | "fail", "continue", or "skip" |
| Custom request metadata | No | Yes (title, summary, metadata) |
| Visible in graph | As a flag on the task | As its own node |
needsApproval. If you’re thinking of it as a fork in the road, use <Approval>.
Structured Human Input: <HumanTask>
Sometimes you don’t need a yes/no — you need the human to provide data. A code review, a triage classification, a budget estimate. <HumanTask> is a task where the human is the agent: the workflow suspends until a human provides JSON matching the output schema, with validation and retries.
maxAttempts (default 10). Schema validation happens at compute time, not at submission time. See the HumanTask component reference for the full API.
Conditional Gates: <ApprovalGate>
What if approval is only needed sometimes? Low-risk deploys sail through; high-risk deploys need a human. <ApprovalGate> wraps <Branch> + <Approval> into a single component:
when is false, the gate auto-approves immediately. When true, it pauses for human review. Both paths write a valid ApprovalDecision to the same output. See the ApprovalGate component reference.
Escalation: <EscalationChain>
When the question isn’t “should a human decide?” but “which agent should handle this, and what happens when it can’t?” — use <EscalationChain>. It runs agents in sequence, escalating to the next level when one fails or its escalateIf predicate returns true. Optionally, the final level is a human approval fallback:
Selection and Ranking
Not every human decision is yes/no.<Approval> supports mode="select" and mode="rank" for choosing among options:
mode="select" returns { selected: string, notes: string | null }. mode="rank" returns { ranked: string[], notes: string | null }. See the Approval component reference for the schemas.
Scoped Approvals and Auto-Approval
For sensitive approvals, restrict who can decide withallowedScopes and allowedUsers. For approvals that become routine, use autoApprove to skip the human after enough consecutive manual approvals:
autoApprove also supports condition (auto-approve when a predicate is true) and revertOn (revert to human approval when conditions change). See the Approval component reference for the full ApprovalAutoApprove type.
Next Steps
- Approvals — Approval nodes, denial policies, and decision values as workflow data.
- Approval Component — Full API reference for
<Approval>. - ApprovalGate Component — Conditional approval gates.
- HumanTask Component — Structured human input with schema validation.
- EscalationChain Component — Multi-level agent escalation with human fallback.
- Suspend and Resume — The underlying durability model.
- Workflow Approval Example — An end-to-end approval flow.