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.

enforcementux

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.

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.

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.

approval handling
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.

What the review queue looks like depends on your use case, but the minimum it needs to capture is:

  • The original requestId from 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.

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 be allowed and 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.
The re-verify approach is simpler and safer. It keeps the permission boundary intact and ensures the decision reflects the current state of the passport, not a snapshot from before the review.

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.