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>.
Next Steps
- Approval Component — Full API reference for
<Approval>. - Suspend and Resume — The underlying durability model.
- CLI Reference — Commands for approving, denying, and resuming workflows.