Skip to main content
Wraps <Branch> + <Approval> into a single component. When when is true, the workflow pauses for human approval. When false, a static task auto-approves immediately so downstream nodes can proceed without delay.

Import

import { ApprovalGate } from "smithers-orchestrator";

Props

PropTypeDefaultDescription
idstring(required)Unique node id within the workflow.
outputz.ZodObject | Table | string(required)Where to persist the approval decision.
request{ title: string; summary?: string; metadata?: Record<string, unknown> }(required)Human-facing approval request.
whenboolean(required)When true, approval is required. When false, auto-approves.
onDeny"fail" | "continue" | "skip""fail"Behavior after denial.
skipIfbooleanfalseSkip this node entirely.
timeoutMsnumberundefinedMax wait in ms. Node fails on timeout.
retriesnumber0Retry attempts before failure.
retryPolicyRetryPolicyundefined{ backoff?: "fixed" | "linear" | "exponential", initialDelayMs?: number }
continueOnFailbooleanfalseWorkflow continues even if this node fails.

Basic usage

Gate production deploys on a risk score. Low-risk changes sail through; high-risk changes require a human sign-off.
const risk = ctx.output(outputs.riskScore, { nodeId: "risk" });

<Workflow name="deploy-pipeline">
  <Sequence>
    <Task id="risk" output={outputs.riskScore} agent={riskAgent}>
      Assess the risk of deploying this changeset.
    </Task>

    <ApprovalGate
      id="deploy-approval"
      output={outputs.deployDecision}
      when={risk.level === "high"}
      request={{
        title: "Approve high-risk deploy?",
        summary: `Risk score: ${risk.score}/100`,
        metadata: { commit: ctx.input.sha },
      }}
      onDeny="fail"
    />

    <Task id="deploy" output={outputs.deploy}>
      {{ deployed: true }}
    </Task>
  </Sequence>
</Workflow>

Auto-approve on dry run

<ApprovalGate
  id="publish-approval"
  output={outputs.publishDecision}
  when={!ctx.input.dryRun}
  request={{ title: "Publish the article?" }}
/>
When dryRun is true, when is false and the gate emits { approved: true, note: "auto-approved" } without pausing.

With timeout and retry

<ApprovalGate
  id="budget-approval"
  output={outputs.budgetDecision}
  when={estimate.total > 10_000}
  request={{
    title: "Approve budget over $10k?",
    summary: `Estimated cost: $${estimate.total}`,
  }}
  timeoutMs={60 * 60 * 1000}
  retries={1}
  onDeny="continue"
/>

How it works

<ApprovalGate> renders a <Branch>:
  • when is true — mounts an <Approval> node that pauses for human review.
  • when is false — mounts a static <Task> that resolves immediately with { approved: true, note: "auto-approved", decidedBy: null, decidedAt: null }.
Both paths write to the same output, so downstream nodes can branch on decision.approved without caring which path was taken.

Notes

  • The auto-approve path produces a valid ApprovalDecision shape, so downstream logic remains uniform.
  • Auto-approve timing lives in Smithers’ internal approval/event records, not in the durable task output.
  • onDeny only applies to the human-approval path. The auto-approve path always succeeds.
  • Combine with skipIf to disable the gate entirely during development.