Approvals
Hold a risky call for a human decision, then approve or deny from the dashboard, a signed email link, or Slack.
A policy whose action is require_approval doesn't block the
call and doesn't let it through: it parks it as a pending approval and asks a
human. The SDK and framework adapters wait the hold out
transparently (they long-poll the hold's status), so to your agent the call just
takes longer and then resolves to the final allow or deny. The remote MCP
proxy waits a short window server-side; if the hold is still open it returns a
denial that tells the agent to retry once a human approves. A card preflight
that comes back review opens the same kind of hold (kind: "card" instead of
"tool_call").
An approved hold resolves to allow; a denied or expired hold resolves to
deny.
Hold timeout
Every hold gets a deadline from the workspace's Approval hold timeout
(Settings → Workspace, 1 to 1440 minutes, default 5 minutes). A hold
still undecided at the deadline auto-denies: it resolves as expired, the
held call is patched to deny, and approval.resolved fires with decision
deny and resolvedBy: "system". Approvals fail closed.
The Expiry reminder (on by default) re-sends the approve/deny prompt shortly before the deadline, with a lead of 60 seconds or 20% of the timeout, whichever is larger, so an auto-deny never lands unseen.
Ways to decide
Every channel resolves through the same vote-casting path, so dual control and deny-overrides behave identically wherever the click happens, and resolution is idempotent: the first verdict wins and later attempts see "already resolved".
Dashboard
The approvals inbox at Dashboard → Approvals lists open holds with their risk score and reason; approve or deny in one click.
Some holds also offer a shortcut that resolves the next occurrence, not just this one: an information-flow hold can mint a scoped allow-rule for that exact flow, and an ordinary tool hold can mint a bounded 60-minute grant for that tool.
A kill-chain hold deliberately offers neither. It is the completing step of a multi-step sequence, so a blanket grant would auto-allow every remaining hop of the very attack the rule just caught. Approving one releases that single call and nothing more. The expanded row shows which sequence was matched and which earlier steps constitute it, and Deny & block remains available.
Signed email links
Approvers get an email with a decide link (/approve/{token}). The token is an
HMAC-SHA256 over the approval id, workspace id, and an expiry, so it can't
be forged or pointed at a different approval, and it stops working after it
expires (1 hour, comfortably past the default hold timeout). The approve/deny choice is
made on the page, never encoded in the token or the URL, so an email
client prefetching the link can't decide anything.
No dashboard login is needed: the token is the authorization. The page posts to
POST /api/approvals/act; an invalid or expired token gets a 401, and a hold
that was already resolved gets a 409.
Slack
Connect Slack and each hold posts a message with Approve and Deny
buttons. Point your Slack app's Interactivity Request URL at
/api/slack/interactivity and set SLACK_SIGNING_SECRET (the endpoint answers
503 until it's configured). Slack's request signature is verified over the
raw body before anything is parsed; the button itself carries the same signed
approval token naming exactly one approval. After the click, the message is
replaced with a status line (the verdict, or the vote count while a second
approver is still needed), and the vote is recorded under the clicking Slack
user's identity.
Dual control
Set Dual control (two-person rule) to a minimum risk score (0 to 100, blank to disable). A hold whose risk is at or above the threshold when it opens requires two distinct approvers instead of one.
- Approve votes are deduplicated per approver, so one person clicking twice still counts once.
- Deny overrides: a single deny from any approver resolves the hold denied immediately, regardless of approve votes already cast.
- Email-link votes are cast under one shared
email-linkidentity, so the email channel contributes at most one approve vote to a two-person hold. Slack votes are per Slack user and dashboard votes are per member, so either can supply the second approver.
Escalation
Set Approval escalation (minutes, blank to disable) and a hold still pending
past the window notifies the second tier: every notification channel subscribed
to approval.escalated, plus an email to the workspace owners (a built-in
fallback that works with no channel configured; the original notice goes to
owners and admins). The window must be shorter than the hold timeout; an
escalation that would land at or after expiry never fires.
Escalation also emits the approval.escalated webhook event:
| Field | Meaning |
|---|---|
workspaceId, approvalId | Which hold escalated. |
kind | tool_call or card. |
subject | Tool name (tool call) or card subject. |
agentId | The agent whose call is held. |
risk | The hold's risk score. |
expiresAt | ISO deadline after which the hold auto-denies (null on a legacy hold). |
Webhook events
| Event | Fires when |
|---|---|
approval.pending | A hold opens and awaits an approver. |
approval.escalated | A hold passes the escalation window still undecided. |
approval.resolved | A hold is approved, denied, or expired (expiry delivers decision deny, resolvedBy: "system"). |
See Webhooks for signatures, retries, and replay.
Next steps
- Policies: author the
require_approvalrules that open holds. - Framework integrations: how adapters surface a hold to your agent.