Approval Gate
Block execution and ask the user to approve a risky or high-stakes agent action.
Install
npx ax-depute@latest add approval-gatepnpm dlx ax-depute@latest add approval-gateyarn dlx ax-depute@latest add approval-gatebunx ax-depute@latest add approval-gateOverview
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
The agent will send a follow-up email to 3 recipients.
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
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Short label for the action being approved |
description | string | — | Human-readable explanation of what the agent will do |
agentReasoning | string | — | Why 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 |
scope | ApprovalScope | — | Optional scoped grant (target, resourceLimit, durationSeconds) |
onApprove | () => void | — | Called when user approves |
onReject | () => void | — | Called when user rejects |
approvalHandoff | ApprovalHandoff | — | Configures timeout and fallback to automatically trigger async handoff. |
onHandoff | (ctx) => Promise<void> | — | Fires when the synchronous timeout is breached. |
handoffDeadlineMs | number | — | Absolute 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 Cardto show intent without requiring an explicit approval - For legally binding or cryptographically signed actions, escalate to
Binding Approvalinstead
Accessibility
- The gate renders as a
role="dialog"region witharia-modal="true"when in blocking mode to trap focus appropriately - Approve and Reject buttons have explicit
aria-labelvalues 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:
Tabmoves between Approve/Reject,Enter/Spacetriggers;Escapedoes 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 TraceIt 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.