mode="approve" writes an ApprovalDecision to the configured output:
decidedAt is reserved for compatibility, but Smithers keeps the actual approval timestamp in internal approval records and the event log instead of the durable task output.
Import
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | (required) | Unique node id within the workflow. |
mode | "approve" | "select" | "rank" | "approve" | Approval shape. "approve" returns a boolean decision, "select" returns one option, "rank" returns an ordered list. |
options | ApprovalOption[] | undefined | Required for mode="select" and mode="rank". |
output | z.ZodObject | Table | string | (required) | Where to persist the decision. Zod schema from outputs (recommended), Drizzle table, or string key. |
outputSchema | z.ZodObject | approvalDecisionSchema | Override the decision schema (manual DB API). |
request | { title: string; summary?: string; metadata?: Record<string, unknown> } | (required) | Human-facing request. title becomes the node label. |
onDeny | "fail" | "continue" | "skip" | "fail" | Behavior after denial. "continue" and "skip" still persist the denial. |
allowedScopes | string[] | undefined | Optional gateway scopes allowed to decide this approval. |
allowedUsers | string[] | undefined | Optional gateway user IDs allowed to decide this approval. |
autoApprove | ApprovalAutoApprove | undefined | Auto-approval policy. Supports immediate auto-approval, approval-after-history, and audited auto-approvals. |
async | boolean | false | When true, unrelated downstream flow can continue while this approval is pending. Explicit dependencies still wait for the resolved decision. |
dependsOn | string[] | undefined | Task IDs that must complete first. |
needs | Record<string, string> | undefined | Named deps. Keys become context keys, values are task IDs. |
skipIf | boolean | false | Skip this node entirely. |
timeoutMs | number | undefined | Max wait in ms. Node fails on timeout. |
retries | number | 0 | Retry attempts before failure. |
retryPolicy | RetryPolicy | undefined | { backoff?: "fixed" | "linear" | "exponential", initialDelayMs?: number } |
continueOnFail | boolean | false | Workflow continues even if this node fails. |
cache | CachePolicy | undefined | { by?: (ctx) => unknown, version?: string }. Skip re-execution on cache hit. |
label | string | request.title | Display label override. |
meta | Record<string, unknown> | undefined | Extra metadata merged with request fields. |
Schema-driven Example
Manual API Example
PassoutputSchema={approvalDecisionSchema} when output is a Drizzle table.
Selection and ranking modes
<Approval> can also return typed non-boolean outputs.
mode="select"returns{ selected: string, notes: string | null }mode="rank"returns{ ranked: string[], notes: string | null }
Scoped approvals and auto-approval
allowedScopesandallowedUsersare enforced byGateway.autoApprove={{ after: N }}auto-approves afterNconsecutive manual approvals for the same workflow node.audit: truepreserves an approval record and emitsApprovalAutoApproved.
ApprovalAutoApprove type:
| Field | Description |
|---|---|
after | Auto-approve after this many consecutive manual approvals for the same node. |
condition | Predicate evaluated at render time. When it returns true, the node is auto-approved immediately without waiting for human input. |
audit | When true, an approval record is written and ApprovalAutoApproved is emitted even for auto-approvals. Defaults to true. |
revertOn | Predicate evaluated at render time. When it returns true, a previously triggered auto-approval is reverted and the node goes back to waiting for human input. |
condition and revertOn are re-evaluated each render, so they can react to upstream task output or workflow state.
Behavior
- Workflow enters
waiting-approvalwhen this node is reached. - With
async, the run can keep traversing unrelated later nodes while this approval is pending. smithers approve/smithers denyupdates the record durably.- On resume, the node resolves to a decision object; downstream JSX branches on the value.
onDeny="fail"— hard gate.onDeny="continue"— branch ondecision.approved.- Use
ctx.outputMaybe(...)when branching on an async approval’s output, since the decision may not exist yet during earlier renders.
Metrics
Async approvals contribute to the Prometheus gaugesmithers_external_wait_async_pending{kind="approval"} while waiting for human input.
Durable deferred resolution
<Approval> uses a durable deferred mechanism to survive process restarts. When the node enters waiting-approval state, an @effect/workflow DurableDeferred is created and awaited by the executing task fiber. The deferred is keyed to the run, node, and iteration, so it survives process restarts: if the worker crashes while waiting, the next worker that picks up the task will re-await the same deferred and receive the resolution as soon as a human submits a decision.
When smithers approve or smithers deny is called, bridgeApprovalResolve resolves the deferred, which unblocks the awaiting fiber and lets the compute function proceed to read the decision from the database. No polling is needed.
<Approval> vs needsApproval
| Use | When |
|---|---|
<Approval> | Decision must be persisted as data and consumed by downstream nodes. |
needsApproval on <Task> | Simple pause before a task; no separate decision value needed. |