{"slug": "ai-agent-approval-workflows-handling-edits-and-retries", "title": "AI agent approval workflows: handling edits and retries", "summary": "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.", "body_md": "# AI agent approval workflows: handling edits and retries\n\nAdd human approval to AI agent tool calls. Learn how to review exact requests, resume execution, and handle edits and retries with Airlock.\n\nApproving 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.\n\n[WorkOS Airlock](https://workos.com/airlock) can require human approval before a governed agent call proceeds. It is [available in early access](https://workos.com/airlock). 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.\n\n## Save the action the reviewer will approve\n\nConsider 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.\n\nThis 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.\n\nThe 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.\n\n``` python\ndef create_refund(ctx, refund_id):\n    return create_once(\n        key=(ctx.tenant, ctx.user, refund_id),\n        context=ctx,\n        request={\n            \"method\": \"POST\",\n            \"url\": BILLING_REFUNDS_URL,\n            \"body\": {\n                \"operation_id\": refund_id,\n                \"charge\": \"charge-example-17\",\n                \"amount_cents\": 120000,\n                \"currency\": \"usd\",\n            },\n        },\n    )\n```\n\nUse 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](https://docs.stripe.com/api/idempotent_requests). Adding the header to the agent's Airlock request does not establish duplicate protection.\n\nShow 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.\n\n## Resume the saved tool call after approval\n\nAn 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.\n\n``` python\ndef advance_refund(key, ctx):\n    with operation_lock(key):\n        op = load_owned(key, ctx)\n        if op.state not in (\"ready\", \"pending\"):\n            return\n\n        if not original_context_valid(op, ctx):\n            stop_for_new_authorization(op)\n            return\n\n        if op.approval_id is not None:\n            status = read_approval(op, ctx)\n            if status != \"approved\":\n                if status in (\n                    \"denied\", \"canceled\", \"expired\",\n                ):\n                    stop_attempt(op, status)\n                return\n\n        send_once(op, ctx)\n```\n\nThe 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.\n\n`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.\n\nIf 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.\n\n## Handle the result without submitting the refund twice\n\nA 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:\n\n``` python\ndef send_once(op, ctx):\n    mark_in_flight(op)  # Durable before sending.\n    try:\n        result = submit_via_airlock(\n            op, ctx,\n            approval_id=op.approval_id,\n        )\n    except TransportFailure:\n        mark_unknown_and_reconcile(op)\n        return\n\n    if result.kind == \"needs_approval\":\n        save_pending(op, result.approval_id)\n    elif result.kind == \"not_forwarded\":\n        stop_attempt(op, result.reason)\n    elif result.kind == \"completed\":\n        save_completion(op, result.provider_id)\n    else:\n        mark_unknown_and_reconcile(op)\n```\n\n`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.\n\nTransport 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.\n\nA 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.\n\nTest 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.\n\nThe [Agent Night demo](https://workos.com/blog/agent-night-recap-airlock-intent-based-access-control) shows approval and client resume with an email to an unfamiliar distribution list. The recording below shows that workflow.\n\n[Request Airlock early access](https://workos.com/airlock) to add human approval to your agents' actions.", "url": "https://wpnews.pro/news/ai-agent-approval-workflows-handling-edits-and-retries", "canonical_source": "https://workos.com/blog/ai-agent-approval-policies-airlock", "published_at": "2026-09-15 15:02:39+00:00", "updated_at": "2026-09-23 02:24:58.800922+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-products", "developer-tools"], "entities": ["WorkOS", "Airlock", "Stripe"], "alternates": {"html": "https://wpnews.pro/news/ai-agent-approval-workflows-handling-edits-and-retries", "markdown": "https://wpnews.pro/news/ai-agent-approval-workflows-handling-edits-and-retries.md", "text": "https://wpnews.pro/news/ai-agent-approval-workflows-handling-edits-and-retries.txt", "jsonld": "https://wpnews.pro/news/ai-agent-approval-workflows-handling-edits-and-retries.jsonld"}}