AI Agent Human Approval: Binding Decisions to Exact Actions
AI agent human approval can look safe while authorizing the wrong operation. A reviewer sees "send the refund email," clicks approve, and the workflow later executes changed arguments against a newer customer record. The pause worked, but the authorization boundary did not. Fix this by treating approval as a short-lived, single-use record bound to the exact tool call, actor, tenant, target version, and policy that the reviewer saw. This guide provides the data model, execution sequence, failure rules, and tests needed to keep approval valid across queues, restarts, retries, and concurrent changes.
Why an approve button is not an authorization boundary
Agent frameworks provide useful pause and resume machinery. The OpenAI Agents SDK human-in-the-loop flow can interrupt a run for a tool call, serialize its state, store per-call decisions, and resume after approval or rejection. LangGraph interrupts persist graph state under a thread identifier and support approval, editing, parallel interrupts, and later resumption.
The runtime handles stopping and continuing. Your application still has to record exactly what the person authorized.
A boolean such as approved: true loses the evidence needed to answer that question. It does not identify the tool, canonical arguments, customer or tenant, acting user, target record version, policy version, reviewer, or expiry. It also says nothing about whether the decision has already been consumed.
This distinction matters for any operation that sends a message, updates a record, publishes content, deletes data, grants access, or moves money. OWASP's Excessive Agency guidance recommends minimizing tool functionality and permissions, executing in the user's authorization context, and requiring user approval for high-impact actions. Human review is one control within that design. It cannot replace server-side identity, authorization, validation, or state checks.
Define the approval contract
A valid approval should authorize one precise execution attempt under explicit conditions. Store it as an immutable decision record rather than a mutable flag on the agent run.
The record needs these fields:
- A unique approval ID and workflow run ID
- The exact tool identity and a tool schema or implementation version
- Canonical arguments and their digest
- Tenant and acting principal
- Target resource identities and versions
- The policy version used to require and evaluate approval
- Reviewer identity, decision, reason, and decision time
- Expiry time and current lifecycle state
- A one-time execution claim and resulting operation identity
Use a small lifecycle: pending, approved, rejected, expired, claimed, then executed or outcome_unknown. Do not move a rejected or expired record back to approved. Create a new approval if the action changes.
A practical record can look like this:
{
"approval_id": "apr_7f2",
"run_id": "run_812",
"tool": "send_refund_email",
"tool_version": "3",
"canonical_arguments": {
"customer_id": "cus_1042",
"case_id": "case_88",
"recipient": "[email protected]",
"refund_amount": "125.00",
"currency": "USD",
"template_id": "refund-approved-v4"
},
"action_digest": "sha256:stored-server-side",
"tenant_id": "tenant_19",
"acting_principal": "user_51",
"target_versions": {
"case_88": "17",
"cus_1042": "9"
},
"policy_version": "refund-policy-12",
"status": "approved",
"reviewer_id": "reviewer_6",
"decided_at": "2026-08-24T09:15:00Z",
"expires_at": "2026-08-24T09:30:00Z",
"claimed_at": null,
"operation_id": null
}
The digest is not a substitute for the canonical fields. Keep both. The fields make the decision inspectable, while the digest gives the executor a compact equality check. Build the digest on the server from a deterministic serialization. Normalize field order and data types, but do not normalize away meaningful distinctions such as tenant, currency, case, recipient, or resource version.
Build the AI agent human approval sequence
Implement the boundary in seven steps. The model may propose an action, but trusted application code owns every transition after that proposal.
Parse and validate before requesting review
Parse tool arguments against the server's current schema. Reject malformed JSON, non-object input, unknown fields, invalid values, and unauthorized targets before creating an approval.
Fail closed when parsing cannot establish what would run. A reported OpenAI Agents SDK issue showed a callable approval gate receiving an empty object after malformed JSON, allowing a content-based predicate to return false. The issue is closed, and current SDK documentation states that malformed or non-object arguments require manual approval. Keep the failure shape in your own regression suite because framework upgrades and custom wrappers can reintroduce it.
Approval should never turn invalid input into valid authority. If arguments are unreadable, there is nothing precise for a person to approve.
Resolve identity and state on the server
Attach tenant and acting principal from authenticated server context, not from model arguments. Load every affected resource and capture the versions used to build the proposed action. Run normal authorization and policy checks before review so the interface does not ask a person to approve an operation the actor could never perform.
The reviewer is not lending their permissions to the agent. Their decision authorizes the proposed operation only if the original actor still has the required access when execution begins.
Render a faithful preview
Generate the review screen from canonical server fields. Do not ask the model to summarize its own tool call and then treat that summary as the approval target.
Show every field that can materially change the result. For a message, include recipient, channel, subject, body or template, attachments, and associated business record. For a financial operation, include amount, currency, destination, source account, fees, and business reference. For a record update, show the target, old values, new values, and version.
Display the actor, tenant, policy reason, and expiry. If the interface offers editing, editing must create a new canonical action and digest. It must not mutate an already approved record.
Record the decision as an event
Authenticate the reviewer and check that they are eligible under the current policy. Store approve or reject as an append-only event with the reviewer identity, timestamp, reason, action digest, and visible target versions.
Use a compare-and-set transition from pending to the decision state. Two reviewers clicking at once should produce one accepted transition and one harmless conflict, not two executions. If multiple approvals are required, model each requirement explicitly and derive final authorization only after the necessary distinct decisions exist.
Revalidate immediately before execution
The executor should reload the approval and reconstruct the candidate action from trusted data. It then verifies:
- Status is
approvedand the record has not expired. - Tool identity, version, canonical arguments, actor, tenant, and policy produce the stored digest.
- The actor and reviewer still satisfy current authorization rules.
- Every target resource still matches its captured version or business precondition.
- No prior execution claim or operation result exists.
If any material field or precondition changed, stop and create a fresh preview. Do not silently refresh the target version while keeping the old approval. That would make the review screen historical evidence for a different action.
Claim once, then execute
Atomically transition the approval from approved to claimed and assign a stable operation ID. Only the worker that wins this transition may call the side-effecting tool. Pass the operation ID to the downstream API as its idempotency key when that API supports one.
Do not hold a database transaction open during a network call. Persist the claim first, execute outside the transaction, then record the result. A retrying worker will see the existing claim and reconcile rather than creating another send or write.
Record the outcome and evidence
Store the downstream operation identifier, response category, completion time, and target version produced by the operation. Avoid copying secrets or sensitive message content into general logs. The audit record should still let an investigator connect the proposal, preview, reviewer decision, execution claim, and business result.
Handle drift, resume failures, and unknown outcomes
A waiting approval can outlive the state that justified it. If the customer case changes, the actor loses access, the policy changes, or the tool implementation changes, expire the decision and request review again. Choose a short expiry for high-impact actions and a longer one only when the captured preconditions make delayed execution safe.
Resume paths need integration tests, not just unit tests around the approval button. One OpenAI Agents SDK practitioner report reproduced a serialized approval that was not honored when the resumed run carried actor context. Treat this as an author report about specific SDK versions, not a current universal defect. Test pause, serialization, identity restoration, decision application, and tool execution as one path.
LangGraph documents that a resumed node starts again from the beginning of the interrupted node. Code before the interrupt can therefore run again. Keep side effects after the approval gate, and make any preparation before it pure or idempotent. When parallel branches pause together, pair each decision with its interrupt identity rather than applying one generic response to every waiting action.
A network timeout after the downstream call creates an unknown outcome. Do not move the approval back to approved. Keep it claimed, query the destination using the stable operation ID, and mark either executed or outcome_unknown. Escalate when the provider offers no reconciliation surface. A second approval cannot prove that the first call did not succeed.
Rejection and expiry are terminal for the action digest. If the agent wants to propose a revised action, it must produce a new approval record. Never let a model reinterpret a rejection as permission to choose slightly different arguments.
Verify the approval boundary
Test the persisted transaction, not only the user interface. Your suite should include these cases:
- Valid approval executes the exact previewed action once.
- Malformed and non-object arguments fail closed before review.
- A changed recipient, amount, tool version, tenant, or actor changes the digest.
- A target record update during review forces a new approval.
- Expired and rejected decisions cannot be claimed.
- Two workers racing to execute produce one winning claim.
- Two reviewers racing to decide produce one valid state transition.
- A process restart preserves the decision and acting identity.
- Parallel interrupts apply decisions to the correct calls.
- A retry after an ambiguous timeout reconciles by operation ID.
- Revoked actor or reviewer permissions block execution.
- Logs connect proposal, decision, claim, and result without exposing secrets.
Run these tests against the real queue, persistence layer, and tool adapter. Mock only the final external service when necessary. A test that calls approve() and asserts a boolean changed does not exercise serialization, concurrency, target drift, or execution.
Add one invariant to production monitoring: no side-effecting tool may start without exactly one unexpired, digest-matching, successfully claimed authorization record when policy requires review. Alert on parse failures, digest mismatches, stale target versions, repeated claims, and unknown outcomes separately. Each signal points to a different repair path.
Common approval mistakes
Do not use a run-wide always approve choice for tools that can act on changing business data. The OpenAI Agents SDK supports sticky decisions during a run, which can be useful for low-risk repeated calls. For sensitive mutations, per-call approval is easier to explain and audit.
Do not place the approval record only inside serialized agent state. Keep the authorization in an application-controlled store with its own uniqueness constraints and lifecycle. Agent state can reference the approval ID, but it should not be able to manufacture or rewrite the decision.
Do not rely on display text as canonical input. Review screens should render trusted structured fields, and execution should rebuild the same structure. Also avoid approving a broad objective such as "resolve this account." Approve the concrete tool action that will create the side effect.
Human approval cannot compensate for excessive permissions. Keep tools narrow, credentials scoped, and downstream authorization active. A careful reviewer can still click the wrong button, and a compromised interface can still misrepresent an overpowered tool.
Test one approval path now
Choose one production or staging action that writes data. Capture its canonical arguments, actor, tenant, target versions, policy version, reviewer decision, expiry, execution claim, and downstream operation ID. Then change one material field after approval and prove that execution stops.
If the changed field does not stop execution, the approval is only a workflow pause. Fix the binding before adding race, restart, expiry, and unknown-outcome cases or enabling another side-effecting tool.
References
- OpenAI Agents SDK human-in-the-loop supports per-call interruptions, approval decisions, serialized run state, sticky decisions, and fail-closed argument handling.
- LangGraph interrupts supports durable pause and resume behavior, thread identity, parallel interrupt mapping, and the documented restart behavior of resumed nodes.
- OWASP LLM06:2025 Excessive Agency supports minimizing tool functionality and permissions, preserving user authorization context, and requiring review for high-impact actions.
- OpenAI Agents SDK issue 3863 provides the practitioner-reported malformed-argument approval failure and its fail-closed regression shape.
- OpenAI Agents SDK issue 4244 provides the practitioner-reported serialized approval and actor-context resume failure.