Depute Logo

Approval Gate

Block execution and ask the user to approve a risky or high-stakes agent action.

Install

npx ax-depute@latest add approval-gate
pnpm dlx ax-depute@latest add approval-gate
yarn dlx ax-depute@latest add approval-gate
bunx ax-depute@latest add approval-gate

Overview

Approval Gate is the trust boundary primitive. It pauses agent execution and surfaces a structured approval UI before the agent crosses a trust boundary — sending emails, writing to a database, calling an external API.

The key design principle (from Stripe's SPT pattern): approvals are scoped grants, not binary yes/no decisions. A user approving "send emails" shouldn't be approving "send unlimited emails forever."

Send external email

Approval Required

The agent will send a follow-up email to 3 recipients.

Agent Reasoning
The task requires notifying stakeholders of the completed analysis.
88% confidence
Interactive StorybookView all states, toggle props, and test edge cases.

Basic usage

<ApprovalGate
  title="Send email to customer"
  description="The agent will send a follow-up to 3 recipients."
  agentReasoning="Task requires notifying stakeholders of completed analysis."
  status="pending"
  onApprove={() => agent.continue()}
  onReject={() => agent.abort()}
/>

With scoped grant

<ApprovalGate
  title="Charge payment method"
  description="The agent will charge $49.00 to the customer's card on file."
  agentReasoning="Subscription renewal detected. Auto-charge is enabled for this account."
  mode="simple"
  status="pending"
  scope={{
    target: 'Stripe API',
    resourceLimit: 4900,     // in cents
    durationSeconds: 300,    // expires in 5 minutes
  }}
  onApprove={() => agent.continue()}
  onReject={() => agent.abort()}
/>

Staged mode (Preview → Confirm → Execute)

For high-risk multi-step actions, use mode="staged" to walk the user through three phases before execution:

<ApprovalGate
  title="Deploy to production"
  description="The agent will deploy the current build to the production environment."
  agentReasoning="All tests passed. Deployment window is clear."
  mode="staged"
  status="pending"
  onApprove={() => agent.deploy()}
  onReject={() => agent.cancelDeploy()}
/>

Async Approval Handoff (Dispatch Pattern)

If an agent executes a long-running background workflow, the human may step away from their machine. A blocking modal that nobody is watching acts indistinguishably from a hung process.

To solve this, configure the approvalHandoff prop to automatically hand the approval off to a mobile device or Slack channel after a set timeout:

<ApprovalGate
  title="Execute Production Failover"
  status={status}
  approvalHandoff={{
    timeoutMs: 30000,           // 30 seconds of inactivity triggers handoff
    fallbackBehavior: 'block',  // What happens if the async deadline passes
  }}
  onHandoff={async (ctx) => {
     await sendSmsWebhook(user.phone, ctx);
     setStatus('handoff_pending');
  }}
  handoffDeadlineMs={Date.now() + 86400000} // Backend TTL for UI rehydration sync
/>

When status shifts to handoff_pending, the component drops the blocking modal trap and gracefully collapses into a non-blocking ambient status card that counts down to the deadline.

Props

PropTypeDefaultDescription
titlestringShort label for the action being approved
descriptionstringHuman-readable explanation of what the agent will do
agentReasoningstringWhy the agent thinks this action is necessary
mode'simple' | 'staged''simple'Staged adds Preview → Confirm → Execute steps
status'pending' | 'approved' | 'rejected' | 'handoff_pending' | 'handoff_denied' | 'handoff_expired''pending'Controls gate state
scopeApprovalScopeOptional scoped grant (target, resourceLimit, durationSeconds)
onApprove() => voidCalled when user approves
onReject() => voidCalled when user rejects
approvalHandoffApprovalHandoffConfigures timeout and fallback to automatically trigger async handoff.
onHandoff(ctx) => Promise<void>Fires when the synchronous timeout is breached.
handoffDeadlineMsnumberAbsolute unix timestamp for TTL enforcement (useful for UI sync on session restore).

When to use

  • The agent is about to cross a trust boundary — sending an email, modifying a database record, calling an external API, executing a financial transaction
  • The action is irreversible or hard to undo and a human should explicitly sign off before it runs
  • You need a scoped, time-limited capability grant rather than a blanket yes/no decision
  • The agent has low confidence or is operating in an unfamiliar context

When not to use

  • The agent is performing read-only operations — no approval gate is needed for purely observational tool calls
  • Every single agent step requires approval — this creates approval fatigue; gate only the high-stakes boundaries
  • The action is trivially reversible (e.g., drafting a note that hasn't been sent) — use Plan Card to show intent without requiring an explicit approval
  • For legally binding or cryptographically signed actions, escalate to Binding Approval instead

Accessibility

  • The gate renders as a role="dialog" region with aria-modal="true" when in blocking mode to trap focus appropriately
  • Approve and Reject buttons have explicit aria-label values that include the action name (e.g., "Approve: Send email to customer")
  • Status changes (approved, rejected, handoff_pending) are announced via aria-live="assertive" on the status region
  • All scoped grant details are exposed as readable text, not just visual badges
  • Keyboard navigation: Tab moves between Approve/Reject, Enter/Space triggers; Escape does not dismiss (intentional — prevents accidental closure)

Solution Patterns

Approval Gate usually sits immediately after a Plan Card and before Run Controls:

Plan Card → [Approval Gate] → Run Controls → Tool Trace

It pauses execution and requires a human cryptographic signature (or explicit click) to proceed.

Design rationale

Why scope? Binary approve/reject is the wrong mental model for agents. Stripe never approves an action with a simple yes/no — they issue capability grants scoped to a specific target, resource limit, and time window.

Why mode="staged"? Agent flows are state machines, not request-response cycles. staged mode maps to Stripe's Authorization Chain pattern: Intent → Plan → Permission Check → Execution → Audit. Each phase can fail or pause for human input.

For irreversible or legally binding actions, consider using Binding Approval instead. It extends the approval pattern with mandatory term acknowledgment, an isSigning freeze state, and signer identity capture. See the decision rule for when to escalate.

On this page