Requires approval: when agents should pause before acting
Not every action is a binary allow-or-deny. Some actions are within scope but high enough risk that a human should review before the agent proceeds. That's what requiresApproval is for — and wiring it correctly is different from wiring a denial.
BehalfID verify decisions have three outcomes: allowed, denied, and requires_approval. Most integrations handle the first two immediately. The third is where most implementations skip a step.
requires_approvalis not a soft denial. It means the action is within the agent's scope but should not execute until a human reviews it. The agent pauses. The request surfaces for review. Execution resumes — or is cancelled — based on that review.
When to use requiresApproval
Set requiresApproval: true on a permission scope when the action is legitimate but carries enough risk that autonomous execution is unacceptable without a checkpoint.
- Large financial transactions above a configured threshold — purchases above $500, wire transfers, subscription sign-ups.
- Irreversible actions — deleting data, sending external communications, granting third-party access, modifying account settings.
- Actions outside normal operating hours or geographic context.
- First-time actions from a newly created or recently modified agent.
The distinction from blockedActions is intent: blockedActions means the agent must never do this. requiresApproval means the agent can do this, but not autonomously.
Handling it in code
The wrong pattern is treating requires_approval like denied — throwing immediately and discarding the request. That loses the context the reviewer needs to make a decision.
const decision = await behalf.verify({
agentId,
action: "purchase",
vendor: "coachella.com",
amount: 742
});
if (decision.decision === "requires_approval") {
// Don't throw. Enqueue for human review.
await reviewQueue.push({
requestId: decision.requestId,
agentId,
action: "purchase",
vendor: "coachella.com",
amount: 742,
reason: decision.reason
});
return { status: "pending_approval", requestId: decision.requestId };
}
if (!decision.allowed) {
throw new Error(decision.reason);
}
// Explicit allow — proceed.
await charge(vendor, amount);The agent suspends and returns a pending state to the caller. The review queue entry carries enough context — action, vendor, amount, agent, and the original requestId — for a reviewer to make a decision without needing to re-fetch anything.
The review queue
What the review queue looks like depends on your use case, but the minimum it needs to capture is:
- The original
requestIdfrom BehalfID for audit linkage. - Enough action context for the reviewer to understand what they're approving.
- A way to resume or cancel the paused agent task when the review completes.
- A timeout — if no review happens within N hours, the request should expire.
BehalfID emits a verification.requires_approval webhook event for each decision. Wire this to your review pipeline so reviewers are notified in real time rather than polling.
After the review
Once a reviewer approves, your system has two options depending on how you built the agent task:
- Re-verify before resuming. Call
behalf.verify()again with the same parameters. If the scope has been updated to allow the action (or the agent re-evaluated its risk), the decision will beallowedand execution can continue. - Bypass with an explicit approval token.If you build a review system where approval grants a short-lived token, the agent can proceed without re-verifying. This requires careful token handling and is only appropriate when re-verification isn't practical.
If the reviewer rejects, cancel the pending task and notify the agent. Log the rejection — a pattern of rejections on the same action is a signal to tighten the scope or add it to blockedActions.