AI agent security

How to Add Human Review and Approval Gates to AI Agent Actions

Build the gate in three parts: an allow/deny check at the action, a hold that persists the intent without executing, and a resolution path. Arcjet returns ALLOW or DENY; the queue and interface are yours.

7 min read
In short: Build the gate in three parts: an allow/deny check at the action, a hold that persists the intent without executing, and a resolution path. Arcjet returns ALLOW or DENY; the queue and interface are yours.

How do you add human review and approval gates to AI agent actions?

You build the gate yourself, in three parts. First, a policy check at the action that returns allow or deny. Then a hold that stores the intent without executing it, and a resolution path where a human approves, rejects, or lets it expire.

This is a pattern rather than a product. Arcjet returns ALLOW or DENY; there is no NEEDS_APPROVAL conclusion. Your code can treat a deny, or an unconditional if, as a hold. The queue and the approval interface are yours to build. Being explicit about that boundary avoids confusion in design review, so this guide describes a pattern that you build yourself.

Actions that need a person

A few actions warrant human review:

  • Refunds above a threshold
  • Changes to payment details
  • Deletion of a customer record
  • Messages sent to every user on a list

For a small set of actions, the right control is not a better filter. It is a person. No detection model reaches the confidence you want before money moves irreversibly, and waiting for one to arrive is not a plan.

The engineering question is where the gate goes, and what decides that a given action needs one.

The pattern

PartWhat it doesWho builds it
Policy check at the action

Returns allow or deny; your code decides whether a deny (or an unconditional check) becomes a hold

Arcjet provides the allow/deny decision and the record
The hold

Stores the intended action and its arguments without executing, so that you can resume it later

You, in your own datastore
Resolution path

Approve, reject, or expire, plus the execution step that runs only on approval

You, in your own workflow and interface

Use a decision as the trigger

import { launchArcjet, tokenBucket } from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const refundVolume = tokenBucket({
refillRate: 5,
intervalSeconds: 3_600,
maxTokens: 10,
});
const decision = await arcjet.guard({
label: "tools.issue-refund",
actor: session.userId,
correlationId: workflowRunId,
rules: [refundVolume({ key: session.userId, requested: 1 })],
metadata: { amount_cents: amountCents, currency },
});
if (decision.hasFailedOpen()) {
throw new Error("Security check unavailable");
}
if (decision.conclusion === "DENY") {
// Do not execute. Persist the intent and hand it to a human.
return queueForHumanReview({
action: "issue-refund",
args: { amountCents, currency, invoiceId },
actor: session.userId,
workflowRunId,
decisionReason: decision.reason,
});
}

The check is not the gate. It returns allow or deny, and it leaves a record of the attempt with its label and metadata attached. Everything after the return is code that you write, including the decision to treat a deny as a hold rather than a hard stop.

For actions that always require approval regardless of volume or content, you do not need a rule at all. Use your own irreversibility classification and queue directly. You do not need to delegate every decision in your application to a policy engine. An unconditional gate expressed as a plain if is easier to review than one hidden in remote configuration.

Choose what to gate

Gate on consequence, not on model confidence. A confident agent doing something irreversible is more dangerous than an uncertain one doing something cheap.

Start with reversibility. Can this be undone cheaply? The usual candidates are money movement, changes to credentials and contact details, deletions, and anything that fans out to many recipients. Everything else can run, with enforcement and a record.

Keep the gated set small. An approval queue that nobody has time to clear is worse than no gate at all. It manufactures evidence of a review that did not happen. If your reviewers are approving more than a handful of items a day without reading them, the gate has stopped being a control and become a formality.

Design the expiry path deliberately

Every held action needs an answer to "what happens if nobody responds?"

Expiring to rejection is the safe default, and we recommend it for anything irreversible. Expiring to approval is occasionally correct for low-stakes actions where the cost of not acting exceeds the cost of acting. Make that a deliberate decision recorded in the code rather than an accident of a queue with no timeout.

Also decide what the agent tells the user while an action is held. A workflow that silently stalls looks like a bug and generates a support ticket. Returning "this needs approval, here is the reference" turns the gate into a visible product behavior.

Evidence for the review

Give the check a label that names the action. Pass metadata describing the context: the actor, the workflow run, and the server-controlled values that make the attempt legible, such as an amount or a target account identifier.

Keep user-authored free text out of metadata. It is customer-supplied and untrusted downstream. It is also the part of a record most likely to leak something, or to carry an injection payload into whatever reads it later.

Arcjet records the decision that triggered the hold, and the record is inspectable, so the review has context attached: what the agent tried, for whom, and which rule flagged it. That record is also what makes "we review high-risk agent actions" demonstrable rather than asserted. For more information about that evidence, see producing compliance evidence for AI agent activity.

What this pattern does not give you

Arcjet does not ship an approval-gate feature. There is no hosted queue, no approval interface, and no built-in resumption of a held workflow. If you need those, you are building them or adopting a workflow engine that provides them.

What you get from the runtime layer is an allow or deny and a record of the attempt, retained according to your plan's log retention (up to 30 days on the Growth plan; longer on Enterprise). Treating a deny as a hold, and keeping a durable copy in your own store, is application code.

Where Arcjet fits

Approval workflows are usually built on a workflow engine or a task queue, and that is still true here.

Arcjet provides the decision and the record: allow or deny at the action that your code acts on. The decision carries a label naming the operation and metadata describing the context, and it is inspectable, so the review has evidence attached. Direct Guard fails open (allow with error codes). The preceding sample refuses to execute when hasFailedOpen() is true. Vercel AI SDK and LangChain wrappers fail closed unless you opt into continuing on error.

Some platforms in this category do ship hold-and-approve flows as a product feature. If a managed approval queue is what you need, that is a real difference. If you want allow/deny plus a recent decision record and you own the workflow yourself, this is the pattern.

Learn more: Arcjet Guards

Frequently asked questions

How do I add human approval gates to AI agent actions?

Build it in three parts: a policy check at the action that returns allow or deny, a hold that persists the intended action without executing it, and a resolution path where a human approves, rejects, or lets it expire. Arcjet has no NEEDS_APPROVAL conclusion; your code may treat a deny as a hold. The queue and interface you build yourself.

Does Arcjet ship an approval-gate feature?

No. There is no hosted queue, no approval interface, and no built-in resumption of a held workflow. What you get is an ALLOW or DENY at the action that your application acts on, plus a record of the attempt with its label and metadata.

Which agent actions should require human approval?

Gate on consequence rather than model confidence, using irreversibility as the test. Money movement, credential and contact-detail changes, deletions, and anything fanning out to many recipients are the usual candidates. Keep the set small, because an approval queue nobody can clear becomes a rubber stamp.

What should happen if nobody responds to an approval?

Expiring to rejection is the safe default and should be the default for anything irreversible. Expiring to approval is occasionally correct for low-stakes actions, but it should be a deliberate decision in the code rather than an accident of a queue with no timeout.

AI runtime security in your code

Protect your AI agent workflows with Arcjet

Get allow, deny, and redact on agent actions before the side effect.