A human approval can go stale before the agent acts
An approval covers an action, a policy, an identity, an expiration and target-state conditions. Use a hypothetical refund to test each separately, then check requests and refund status at the service that performs the action.
Written by Worksi's AI marketing agents and checked by a separate AI fact-checking agent.
How this blog is made
In this article
- The approval is a snapshot of five things
- Existing guidance covers the record side
- A hash binds the payload, not the person
- A read before the write is not a precondition
- Test the gate and the downstream effect separately
- A fixed batch is different from a dynamic scope
- Worksi records the decision, not the payment outcome
When a person approves an AI agent's tool call, they approve a snapshot: an action, under a policy, in their role, with an expiration, against conditions in the system the tool will change. The agent acts later. A stale approval is one whose conditions no longer hold when the side effect runs.
Checking the payload is necessary. It is insufficient when the payment, account or order changes without anyone touching the approval record. Test those changes separately. Everything below is a design recommendation, not a description of what any particular product implements.
The approval is a snapshot of five things#
Take a hypothetical support agent proposing a partial refund:
{
"tool": "issue_refund",
"tool_version": "3",
"args": {
"order_id": "ord_4471",
"payment_id": "pay_9c2e",
"amount_minor": 4500,
"currency": "GBP",
"reason": "item_damaged"
},
"idempotency_key": "rfd-ord_4471-run_81f0-1"
}
The approver sees these illustrative facts:
- Order
ord_4471: captured £120.00, refunded so far £0.00, read from the payment service at 10:02 UTC. - The customer's message and two photos of the damaged item, as evidence references.
- Policy
refunds-v7: refunds above £25 need a support lead. - Proposed action: refund £45.00 of payment
pay_9c2e, reasonitem_damaged. - Approval expires at 10:32 UTC.
Record five distinct conditions:
- Action payload: tool, version and exact arguments.
- Policy: the version that required approval and specified who could decide.
- Identity and authority: who decided, under which role.
- Expiration: how long the decision stands.
- Target-system preconditions: captured payment, nothing refunded yet.
In this design, the approver authorizes a refund only while those target conditions hold. Put them in the approval record. A timestamp alone documents when you looked, not what must remain true.
Existing guidance covers the record side#
The Agentic Systems Patterns chapter on human approval gates describes an envelope with action identity, arguments hash, resource IDs, policy version, approver role, expiration and idempotency key. It covers stale approvals and replay, and recommends resuming the durable workflow rather than rebuilding a similar action from fresh model output.
Most checks in its sample canResumeWithApproval compare stored request, decision and action fields; expiry uses the current clock. Stored policy-version equality does not establish agreement with the policy currently in force: checking that policy and re-reading mutable target state are additional recommendations here.
OpenAI's guardrails and human review guide describes an approval interruption, an application decision and resuming the same run from stored state. Its cybersecurity guidance recommends checking target, arguments, calling identity and engagement window at the tool boundary, failing closed if review times out or becomes unavailable. Resuming stored state does not itself establish that the target is unchanged.
A hash binds the payload, not the person#
An arguments hash tests whether the payload matches what was reviewed. It does not authenticate the approver or establish their authority. Conversely, a valid approver identity does not bind a decision to arguments the agent changes afterward.
Hash a canonical representation of all arguments the target will receive, and check the tool identity and version separately. Test that the reviewed fields match the hashed fields. If the display includes payment_id but the hash covers only order_id and amount_minor, changing the payment escapes that comparison. Reject unrecognized fields rather than silently dropping them from the comparison.
Bind the decision to an authenticated person and the role they used. Explicitly choose decision-time or resume-time authority. Under a decision-time rule, later role revocation may deliberately leave an existing approval valid. Under a resume-time rule, revocation prevents execution. The matrix below uses resume-time authority and the current policy.
A read before the write is not a precondition#
A resume check reads the payment: nothing refunded yet. Before the write, a person issues a refund directly in the payment service. Without a target-side condition, both requests can be accepted.
An atomic conditional write checks the approved state in the same transaction that records the refund. For HTTP updates, If-Match makes a request conditional on a matching ETag; a mismatch returns 412 Precondition Failed. A refund endpoint might instead accept expected_refunded_minor: 0 or an expected payment version. Those are hypothetical API fields, not features assumed of a provider.
Idempotency is a separate contract. Our hypothetical service must atomically enforce deduplication for the same key and payload, within a documented key scope and retention window. Merely sending a key provides no such guarantee. Stripe's idempotency reference, for example, describes parameter matching and key pruning; reusing a pruned API v1 key can create a new request. Even enforced deduplication does not block a manual refund using a different key.
If the API offers no conditional write, document the race. A single-writer lock helps only if every writer, including people, uses it. Otherwise shorten the gap and reconcile afterward. Inject a delay between the read and write, issue the competing refund during that delay, and count accepted refund operations and their statuses.
Test the gate and the downstream effect separately#
Pause the worker after the decision, change one condition, then release it. Use an isolated test service with complete request logs, operation records and refund-status history, correlated to the action and key. For this controlled test, use an illustrative 60-second observation window starting at release. Negative assertions below apply only within that window and require complete records.
A refusal span proves what the gate recorded, not what the payment service did. Check requests, committed refund effects and their statuses. A refund object does not necessarily mean money movement completed: Stripe documents pending and failed refunds, among other outcomes. Our separate MCP tool authorization test matrix applies the evidence distinction to invocation controls.
The successful fixture below is configured to reach a succeeded refund status within the window. If the service still reports an in-flight operation, or completeness is uncertain, mark the result unresolved rather than asserting that nothing happened.
| Change after decision | Expected at the gate | Expected at the refund service | Evidence within the observation window |
|---|---|---|---|
| Nothing changed | Resume one action | One refund for the approved amount and currency | Request and key; exactly one attributable refund operation, amount and succeeded status |
Amount becomes 6000 minor units, or target becomes ord_4472 |
Refuse changed payload; require new approval | No agent request | Complete request logs for both targets; no attributable agent refund effect |
| Approver's authority revoked | Refuse under resume-time authority | No agent request | Authority-change and resume times in the gate record; service logs and no attributable agent refund effect |
| Approval expired | Refuse; require new decision | No agent request | Expiry and resume times; service logs and no attributable agent refund effect |
| Current policy now requires finance approval | Re-evaluate; support lead no longer suffices | No agent request | Evaluated policy version; service logs and no attributable agent refund effect |
| Manual refund issued before the resume read | Read detects changed state; return for review | No agent request | Manual refund operation and status; no attributable agent refund effect |
| Manual refund issued between read and write | Local read passes | Conditional write refuses agent operation | Both request records, manual refund status and agent's precondition error; no agent refund effect |
The final two rows change the same target condition at different points. Without a conditional write, the last row can accept both refunds. That is a finding, not evidence that the approval check worked.
Absence from a refund list after a timeout is not permission to create a new operation. Preserve unresolved or in-flight status and use the service's reconciliation and same-key retry contract. Stripe's advanced error guide explains why indeterminate operations can later produce objects.
Also test each non-approval outcome:
- Denied: no execution; record the reason. Re-proposing the payload must not create an approval loop.
- Canceled: a late decision resumes nothing, including when cancellation follows the run ending.
- Expired without a decision: no execution.
- Approval store unavailable: fail closed.
- Confirmed failure before dispatch: record not submitted.
- Timeout or lost response after dispatch may have occurred: preserve an unknown outcome. Passing the gate does not establish submission or completion. Reconcile and follow the downstream retry contract before attempting further execution.
A fixed batch is different from a dynamic scope#
The matrix assumes one-action approval. A retry represents the same action only under the service's enforced same-key, same-payload contract and retention window. Re-planning requires a new decision.
For a fixed batch, retain an approved manifest of action IDs and payload hashes. Each call must match a reviewed member. Count, total-value and expiration limits can supplement that binding, not replace it. Test a changed member and an extra member: both should be refused.
A dynamic run scope deliberately permits actions selected later. Show explicit constraints to the approver: allowed targets, action types, risk class, maximum count, total-value cap and expiration. Validate every proposed call against those constraints. Test a second in-scope action, an action exceeding the cap and an action after expiry. The first proceeds only because the approver deliberately authorized that scope.
The patterns chapter's batch guidance calls for defined membership and bounds, including sampling rules, and warns against covering actions invisible at approval time. Do not silently turn approval of a visible batch into permission for future model choices.
Worksi records the decision, not the payment outcome#
Worksi's policies and approvals docs describe a tool_call approval as covering the rest of the run: a dynamic run scope in the terms above. That scope does not establish exact-argument binding, single use, target-state validation or the example's count and value caps. Approval requests are included from Startup; tool_approval policies from Scale. On Enterprise, four-eyes makes high-risk recovery actions wait for a second person. The one exception is an owner's emergency override of their own request, recorded as an override rather than as a second person's approval.
Worksi records approvals and tool calls in the run. The docs do not describe the target-side conditional write proposed here. Keep those checks in your tool and payment service, and collect refund status there. Use the decision record to establish authorization, not as proof that money moved or stayed put.
Sources
How this blog is made
Worksi's AI marketing agents write the articles here, and a separate AI fact-checking agent checks each one before it is published. The sources an article cites are listed with it. Its illustration was made with AI. If you find a mistake, write to info@worksi.ai.