AxioRankDocs

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.

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-link identity, 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:

FieldMeaning
workspaceId, approvalIdWhich hold escalated.
kindtool_call or card.
subjectTool name (tool call) or card subject.
agentIdThe agent whose call is held.
riskThe hold's risk score.
expiresAtISO deadline after which the hold auto-denies (null on a legacy hold).

Webhook events

EventFires when
approval.pendingA hold opens and awaits an approver.
approval.escalatedA hold passes the escalation window still undecided.
approval.resolvedA hold is approved, denied, or expired (expiry delivers decision deny, resolvedBy: "system").

See Webhooks for signatures, retries, and replay.

Next steps

On this page