cd /news/ai-agents/ai-agent-approval-workflows-handling… · home topics ai-agents article
[ARTICLE · art-137726] src=workos.com ↗ pub= topic=ai-agents verified=true sentiment=· neutral

AI agent approval workflows: handling edits and retries

WorkOS released Airlock in early access, a tool that requires human approval before a governed AI agent tool call executes and binds that approval to a single request. The workflow stores an immutable request under a unique tenant, user, and operation key via a create_once helper, so a $1,200 refund approval no longer applies if the agent changes the amount or selects another charge. Airlock's current generic call path forwards caller-supplied Accept and Content-Type headers but drops Idempotency-Key, leaving duplicate protection to the application's billing endpoint, which must deduplicate by operation_id.

read5 min views1 publishedSep 15, 2026
AI agent approval workflows: handling edits and retries
Image: source

Add human approval to AI agent tool calls. Learn how to review exact requests, resume execution, and handle edits and retries with Airlock.

Approving an agent's $1,200 refund should authorize that refund to that customer. If the agent changes the amount or selects another charge before execution, the original approval should no longer apply. This guide walks through an AI agent approval workflow that binds a human decision to one tool call and resumes execution after approval.

WorkOS Airlock can require human approval before a governed agent call proceeds. It is available in early access. The example below shows how your application can preserve the proposed action, resume it after approval, and recover when the provider's response is uncertain.

Save the action the reviewer will approve #

Consider a support agent handling duplicate-charge tickets. Configure a policy that requires billing approval above $500. For a $1,200 refund, the reviewer needs the customer, charge, amount, currency, and billing evidence. They are approving a particular transaction.

This example routes calls through Airlock to a billing endpoint your application owns. Connect it to Airlock and enable enforcement and the required runtime policy. The refund amount is inside the request body, so do not assume it becomes a fixed numeric constraint automatically. A hard ceiling can also be enforced by your billing service.

The Python-like code uses application helpers, not Airlock SDK methods. create_once atomically stores an immutable request under a unique tenant, user, and operation key, returns the existing matching record on restart, and rejects reuse of that key with different request data. New records start in ready. The context comes from authenticated server state and retains the original intent and session; BILLING_REFUNDS_URL is your configured billing endpoint.

def create_refund(ctx, refund_id):
    return create_once(
        key=(ctx.tenant, ctx.user, refund_id),
        context=ctx,
        request={
            "method": "POST",
            "url": BILLING_REFUNDS_URL,
            "body": {
                "operation_id": refund_id,
                "charge": "charge-example-17",
                "amount_cents": 120000,
                "currency": "usd",
            },
        },
    )

Use a globally unique ID for each logical refund. The billing endpoint must durably deduplicate by operation_id and reject changed payloads for the same ID. This is application behavior you implement. Airlock's current generic call path forwards caller-supplied Accept and Content-Type headers; it drops Idempotency-Key. If your billing service calls Stripe, that service supplies the provider key and follows Stripe's idempotency rules. Adding the header to the agent's Airlock request does not establish duplicate protection.

Show the saved transaction and linked charge records to the reviewer. Let Airlock validate the configured approver's identity and authority; an agent-supplied approver name is not sufficient.

Resume the saved tool call after approval #

An approval grants a one-time pass for a matching request under the same organization, user, and intent. The client then retries the saved call. Clicking approve does not execute the refund. On restart, inspect the saved approval before sending anything.

def advance_refund(key, ctx):
    with operation_lock(key):
        op = load_owned(key, ctx)
        if op.state not in ("ready", "pending"):
            return

        if not original_context_valid(op, ctx):
            stop_for_new_authorization(op)
            return

        if op.approval_id is not None:
            status = read_approval(op, ctx)
            if status != "approved":
                if status in (
                    "denied", "canceled", "expired",
                ):
                    stop_attempt(op, status)
                return

        send_once(op, ctx)

The operation lock permits one worker to advance this record at a time. load_owned checks the caller's tenant and user. original_context_valid requires the original, still-valid intent and session. A new intent needs fresh policy evaluation; it cannot spend the old intent's approval. Keep the billing operation ID when arranging a new authorization attempt.

read_approval normalizes the approval status; a pending status or failed status lookup sends nothing. A denied, canceled, or expired approval stops this attempt. A local polling timeout leaves the pending record intact. Even an approved status is provisional: Airlock checks expiry, identity, the request fingerprint, and whether the pass is unspent when the call is retried. Concurrent retries cannot both consume that pass.

If the amount or charge changes, create a separately reviewed proposal. Do not overwrite the saved request or attach its approval to the changed call. Editing an ordinary policy also does not necessarily revoke an already granted pass; the pass can survive that change.

Handle the result without submitting the refund twice #

A call can be authorized and still fail to return a confirmed provider result. Airlock can return an upstream failure response, and a client can lose the connection after the provider accepts the request. Handle both possibilities on the initial submission and on an approved retry:

def send_once(op, ctx):
    mark_in_flight(op)  # Durable before sending.
    try:
        result = submit_via_airlock(
            op, ctx,
            approval_id=op.approval_id,
        )
    except TransportFailure:
        mark_unknown_and_reconcile(op)
        return

    if result.kind == "needs_approval":
        save_pending(op, result.approval_id)
    elif result.kind == "not_forwarded":
        stop_attempt(op, result.reason)
    elif result.kind == "completed":
        save_completion(op, result.provider_id)
    else:
        mark_unknown_and_reconcile(op)

submit_via_airlock sends the saved method, URL, body, and permitted headers with the original intent context and, when present, the saved approval ID. It never retries automatically. Its normalized result names are application conventions: needs_approval records the returned approval; not_forwarded means Airlock definitively refused this submission before forwarding it; completed requires confirmation of the specific refund from the billing service. An allow verdict or an HTTP success status alone is insufficient.

Transport failures, upstream errors, unrecognized responses, and unconfirmed provider results go to reconciliation. The durable in_flight state also prevents a restarted worker from blindly resubmitting after a crash. A recovery worker must inspect the billing operation's status; it must not create another refund. If the outcome remains unknown, leave the operation unresolved.

A consumed approval cannot be spent again. Any later write attempt must obtain authorization and use the same billing operation ID when retrying the same logical refund. Provider idempotency can have a retention window, so it does not replace your operation record or reconciliation.

Test pending, denied, canceled, expired, and consumed approvals; changed requests and intents; concurrent workers; and a crash after the provider accepts the refund. Verify both the authorization outcome and the billing record.

The Agent Night demo shows approval and client resume with an email to an unfamiliar distribution list. The recording below shows that workflow.

Request Airlock early access to add human approval to your agents' actions.

── more in #ai-agents 4 stories · sorted by recency
── more on @workos 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/ai-agent-approval-wo…] indexed:0 read:5min 2026-09-15 ·