Most teams add human approval to an AI agent by putting a confirmation button in front of a tool call. That solves the visible problem: a person said yes. It leaves the hard problems open. What exactly did the person approve? Can the arguments change between approval and execution? What happens if the worker times out halfway through a refund, a deployment or a record deletion? Who owns the personal data stored in the proposal?
This article is for tech leads who are moving an agent from read-only assistance to actions with real consequences. It describes a proposed design, not a product and not an established industry standard: approval bound to an immutable payload hash, a durable tenant-scoped operation identity, an atomic claim before execution, and a reconcile-before-retry rule for uncertain outcomes. The code is illustrative. It shows the invariants, not a drop-in library, and you must adapt it to your datastore, your target system's actual API contract and your legal obligations.
If you have already read our pieces on idempotency in distributed systems and ambiguous timeouts in consequential API calls, this one applies those ideas to a specific boundary: the moment a human approval turns into a side effect.
Key Takeaways
- Approve a hash, not a sentence. The reviewer must approve the exact canonical payload that will execute. Any change creates a new proposal and a new approval.
- Identity before execution. Create a durable, tenant-scoped operation identity when the proposal is stored, not when the model is re-run. Bind it to the payload hash and reject key reuse with a different payload.
- Claim atomically. Use a conditional database update so only one worker can move an approved operation toward dispatch. A read followed by a write is not a compare-and-swap.
- Unknown is its own state. A timeout means completion could not be confirmed. It is not success and it is not verified failure. Reconcile against authoritative state before any retry.
- Acknowledge only after durable commit. Record approval, state change and scheduled work in one transaction, then acknowledge the approver.
- Use go/no-go controls, not a score. Every control in the checklist is required. A high average cannot compensate for a missing reconciliation path.
- Treat identifiers and payloads as data you own. Operation IDs, hashes and stored payloads can be sensitive, and someone must own their retention and deletion.
Why AI Agent Actions Break the Usual Approval Model
Key takeaway: A traditional approval step authorizes a person's intent. An agent's proposal is generated from untrusted text and can be regenerated at any time, so the approval has to bind to the exact bytes that will execute.
In a conventional workflow, a human fills in a form, reviews it and submits it. The payload exists before the approval and does not change afterward. An agent inverts that sequence. The model reads tickets, emails, documents and tool output, then produces arguments for an action. That input may be wrong, ambiguous or deliberately adversarial. The orchestration layer may also re-plan after a retry, a context-window compaction or a model upgrade, and each re-plan can yield slightly different arguments for what looks like the same intent.
The OWASP guidance on Excessive Agency (LLM06:2025) names this class of risk. Among its mitigations are requiring human approval for high-impact actions and applying complete mediation, meaning downstream systems validate each request against policy instead of trusting the model to decide what is allowed. Those are the right instincts. They do not say how to make an approval durable, specific and safe to execute under failure. That gap is where production incidents live.
Terms used in this article
| Term | Meaning in this design |
|---|---|
| Proposal | A stored, canonical description of one intended action. It is created once and never edited. |
| Payload hash | A digest over a canonical envelope: tenant, action type, schema version, payload and target precondition. |
| Approval | A record that a named, authorized person approved one specific payload hash before an expiry. |
| Operation identity | A durable (tenant, operation ID) pair that names one intended side effect across retries and workers. |
| Claim | An atomic, conditional state transition that gives one worker the right to attempt dispatch. |
| Outcome unknown | Completion could not be confirmed. Not success, not verified failure. |
Scope of this guidance
The IETF's Idempotency-Key HTTP header work is relevant when your target is an HTTP API that supports it. Check the document's current status before you cite it as a standard. It does not cover queue consumers, database writes, human approval semantics or agent planning. The design below uses the same idea of a client-supplied operation key, but it extends the contract to everything your own system controls.
A short note on recency
As of the time of writing, OWASP's 2025 edition of its LLM application risks lists Excessive Agency as LLM06. Framework names, tool protocols and model behavior will keep changing. The evergreen rule is this: whatever produces the action, the thing that executes must be exactly the thing a human approved, once, for one tenant, with a recorded outcome.
How Most Teams Gate Agent Actions, and Why Each Option Fails Differently
Key takeaway: Approval on a tool name or a chat message is cheap to build and easy to bypass accidentally. Approval bound to a payload hash costs more up front and removes the largest class of silent mismatch.
We see three common patterns. They are not equally risky, and the cheapest is not always wrong. The comparison below is qualitative: your own incident data, action volume and reversibility should drive the choice.
Decision artifact: three approval patterns compared
| Dimension | A. Conversational confirm | B. Approval on tool or session grant | C. Payload-hash-bound approval with durable operation |
|---|---|---|---|
| What is approved | The model's prose summary | Permission to call a tool, possibly for a session | One canonical payload, identified by hash |
| Build cost | Lowest | Low to moderate | Moderate to high: schema, state machine, reconciliation |
| Risk of executing something other than what was approved | High | Moderate to high | Low, if arguments are loaded only from the approved record |
| Duplicate side-effect risk on retry | High | Depends on the retry layer | Low, with a stable operation identity and reconcile-first rule |
| Reviewer speed | Fastest | Fast | Slightly slower: reviewer sees rendered structured fields |
| Scalability | Scales until the first incident | Scales, with growing blast radius | Scales with worker count because claims are conditional |
| Best fit | Reversible, low-impact actions | Read-only or sandboxed tools | Consequential, hard-to-reverse or financially material actions |
Hidden failure modes by option
- Option A: The summary and the arguments can diverge. The model can also be steered by injected content so that the summary sounds benign while the arguments are not. Nothing in the system can prove which payload the human saw.
- Option B: A grant such as "the agent may issue refunds" is a permission, not a decision about a specific refund. Session-scoped grants quietly turn one approval into many actions.
- Option C: The failure modes move to implementation: weak canonicalization, a reviewer UI that renders something other than the hashed fields, or a reconcile path that does not exist because the target cannot be queried by reference.
Option B is perfectly reasonable for low-consequence, reversible actions, and a hash-bound design for every action would be over-engineering. The line to draw is consequence and reversibility. If you cannot undo the action cheaply, or if it touches money, access, customer-visible communication or deletion, plan for Option C. Our guide to enterprise agent orchestration covers the wider architecture around this decision.
A Proposed Design: Approval Bound to an Immutable Payload Hash
Key takeaway: Separate three things that are usually blended together: the agent's proposal, the human's approval and the system's execution. Connect them only through an immutable hash and a durable operation identity.
This is a proposed design. It combines well-known techniques (canonical serialization, unique constraints, conditional updates, a transactional outbox) into one flow for agent actions. We have not measured it against your workload, so treat the details as a starting point for your own review.
Design principles
- Propose once, persist first. The orchestrator stores the proposal and its operation ID before anyone is asked to approve. A re-run of the planner must load the stored proposal, not create another one for the same intent.
- Hash a canonical envelope. Include tenant, action type, schema version, the payload and any target precondition such as an expected record version. Two logically equal payloads must produce one hash. RFC 8785 (JSON Canonicalization Scheme) defines one way to do this. Avoid floating-point amounts: use integer minor units or strings.
- Render the approval screen from the stored payload. Never from the model's explanation. The reviewer should see the structured fields that will be sent.
- Approve a hash, with an expiry. An approval for hash H is invalid for any other payload and invalid after its expiry.
- Make the stored payload and hash immutable. Any correction is a new proposal with a new operation ID and a new approval.
- Load execution arguments only from the approved record. This closes the time-of-check to time-of-use gap described in CWE-367.
State model
| State | Meaning | Allowed next states |
|---|---|---|
| pending_approval | Stored, waiting for a human | approved, rejected, expired |
| approved | Approval recorded for this exact hash | claimed, expired |
| claimed | One worker holds a lease. Nothing has been sent yet. | dispatching, approved (lease expired before dispatch) |
| dispatching | Request is about to be, or has been, sent | succeeded, failed_terminal, outcome_unknown |
| outcome_unknown | Completion could not be confirmed | succeeded, failed_terminal, approved (only on authoritative proof the effect did not occur and approval is still valid), human escalation |
| succeeded, failed_terminal, rejected, expired | Terminal states | None |
Illustrative schema and hashing
-- Illustrative PostgreSQL-style schema. Adapt types, encryption and RLS to your stack. CREATE TABLE agent_operation ( tenant_id text NOT NULL, operation_id uuid NOT NULL, -- random; never derived from personal data action_type text NOT NULL, schema_version int NOT NULL, payload_hash bytea NOT NULL, -- immutable after insert payload_ref text NOT NULL, -- pointer to tenant-scoped, encrypted payload state text NOT NULL, approval_expires timestamptz NOT NULL, fencing_token bigint NOT NULL DEFAULT 0, lease_expires_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (tenant_id, operation_id) ); -- A trigger (not shown) rejects UPDATEs that change payload_hash or payload_ref. -- Row-level security or an equivalent policy scopes every query to tenant_id. -- Illustrative hashing. json.dumps with sort_keys is NOT full RFC 8785; -- use a JCS library, and keep numbers as integers or strings. import hashlib, hmac, json def payload_digest(tenant_id, action_type, schema_version, payload, precondition, key): envelope = { 'tenant': tenant_id, 'action': action_type, 'schema': schema_version, 'payload': payload, 'precondition': precondition, } canonical = json.dumps(envelope, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8') # Keyed digest if the hash leaves your trust boundary (see privacy section). return hmac.new(key, canonical, hashlib.sha256).digest()Creating the proposal is an insert guarded by the primary key. If the insert affects zero rows, load the existing row and compare hashes. A different hash under the same (tenant, operation ID) is a conflict to reject, not a payload to overwrite. This mirrors how Stripe documents its own behavior: its idempotent requests reference says the idempotency layer compares incoming parameters to the original request and errors if they differ. That is a statement about Stripe's API, not a universal guarantee, so verify the contract of each target you call.
Approval as one atomic transaction
BEGIN; -- 1. Record the approval (unique per tenant_id + operation_id) INSERT INTO approval (tenant_id, operation_id, payload_hash, approver_id, policy_version) VALUES ($1, $2, $3, $4, $5); -- 2. Move state only if the approver saw this exact hash and it has not expired UPDATE agent_operation SET state = 'approved' WHERE tenant_id = $1 AND operation_id = $2 AND payload_hash = $3 AND state = 'pending_approval' AND approval_expires > now(); -- If 0 rows updated: ROLLBACK and tell the approver the proposal changed or expired. -- 3. Schedule the work in the same transaction (transactional outbox) INSERT INTO outbox (tenant_id, operation_id, kind) VALUES ($1, $2, 'execute'); COMMIT; -- Acknowledge the approver only after COMMIT succeeds.Putting approval, state change and scheduled work in one transaction is the same discipline behind the outbox pattern. If the process dies before commit, nothing happened and the approver can retry. If it dies after commit, the outbox row guarantees the work is still scheduled.
Execution Path: Atomic Claim, Verified Acknowledgment, and Unknown Outcomes
Key takeaway: Use a conditional update to claim work, record intent to dispatch before sending, and treat any lease that expires after dispatch as outcome unknown. Never re-send because a timer fired.
Atomic claim with a fencing token
Multiple workers will see the same outbox message. Read-compare-then-write does not protect you: two workers can both read state approved, both decide to proceed and both write. The claim must be a single conditional statement evaluated by the database.
-- Claim: succeeds for exactly one worker UPDATE agent_operation SET state = 'claimed', fencing_token = fencing_token + 1, lease_expires_at = now() + interval '2 minutes' WHERE tenant_id = $1 AND operation_id = $2 AND state = 'approved' AND payload_hash = $3 RETURNING fencing_token; -- no row returned means someone else owns it -- Record intent to dispatch BEFORE the outbound call UPDATE agent_operation SET state = 'dispatching', lease_expires_at = now() + interval '10 minutes' WHERE tenant_id = $1 AND operation_id = $2 AND state = 'claimed' AND fencing_token = $4 RETURNING 1; -- Lease reaper UPDATE agent_operation SET state = 'approved' WHERE state = 'claimed' AND lease_expires_at < now(); -- nothing was sent UPDATE agent_operation SET state = 'outcome_unknown' WHERE state = 'dispatching' AND lease_expires_at < now(); -- may have been sentThe distinction between the two reaper rules is the heart of the design. A lease that expires while the operation is merely claimed is safe to return to approved because no request was sent. A lease that expires while dispatching cannot be returned, because the request may have reached the target. The worker could have crashed one line before or one line after the network call, and the database cannot tell which.
Dispatch, terminal failure and unknown outcomes
def execute(op): token = claim(op) # None if another worker owns it if token is None: return approved = load_payload(op.payload_ref) # only source of arguments assert digest_of(approved) == op.payload_hash # detect storage tampering or drift mark_dispatching(op, token) try: result = target.apply( reference=op.operation_id, # target-side dedupe key if supported payload=approved.payload, expected_version=approved.precondition, # target-side conditional mutation ) except DefinitiveRejection as e: # target proves it did NOT apply finish(op, token, 'failed_terminal', e.code) except (Timeout, ConnectionError, IndeterminateServerError): finish(op, token, 'outcome_unknown') # not success, not failure schedule_reconcile(op) else: finish(op, token, 'succeeded', result.id) def reconcile(op): state = target.lookup_by_reference(op.operation_id) # authoritative read if state.found and state.matches(op.payload_hash): finish(op, None, 'succeeded', state.id) elif state.authoritatively_absent and not approval_expired(op): requeue_same_operation(op) # same identity, never a fresh key else: escalate_to_human(op) # remains outcome_unknownTwo points matter here. First, only a definitive rejection, one where the target proves the effect did not occur, is a terminal failure. Everything else is unknown. Stripe's advanced error handling documentation makes the same distinction for its API: it treats the outcome of a request that returns a 500 as indeterminate, says a connection failure does not reveal whether the request executed, and advises reconciling the original outcome before starting another operation. Second, "authoritatively absent" needs care. If the original request could still land late, an absence read proves nothing. You need either target-side deduplication on the same reference, a fencing check at the target, or an idempotency retention window you know has not been exceeded.
Target-side conditional mutation
Your own database can claim work atomically. The target system is a different matter. If it only offers GET then PUT, a pre-check followed by an update is a race, not a compare-and-swap. Prefer targets that accept a precondition inside the mutation itself: an expected version or ETag, a conditional write, a unique constraint on a client reference or a documented idempotency key. If a target offers none of these, disclose the limitation in the design record. Serialize actions per target resource through a single-writer queue, reduce autonomy for that action, or keep it human-executed.
Legitimate later operations
Deduplication must not swallow real repeat actions. A customer who legitimately receives a second credit next month is a new operation with a new operation ID. Three rules keep this clean:
- Same operation ID and same hash: return the recorded state. It is a retry.
- Same operation ID and different hash: reject as a conflict. It is a bug or an attack.
- New operation ID with similar content: allowed, but show the reviewer any recent operation on the same target so a human can judge whether it is a duplicate. Enforce hard business limits, such as a refund cap per order, in the target or in your policy layer, not in the dedupe table.
For the receiving side of events, our durable webhook receiver guide applies the same "acknowledge after durable record" rule to inbound callbacks, including the chat or ticketing callback that carries your approver's click.
Uncertain Input, Tenant Isolation, and Privacy Ownership
Key takeaway: Uncertain input should produce a question, not a guess. Tenant ID belongs in every key, query and hash. Operation identifiers and payload digests can themselves be sensitive, and someone must own their retention.
Uncertain input handling
An agent that fills in a missing field with a plausible value is manufacturing facts. Give the proposal schema an explicit way to say "I do not know" and refuse to create an approvable operation until required fields are resolved.
| Situation | Wrong response | Safer response |
|---|---|---|
| Amount or recipient is ambiguous in the source text | Pick the most likely value | Create a needs_clarification item with the competing candidates and no approve button |
| Two records match the target description | Choose the first match | Present both; require the human to select one and generate a new proposal |
| Source content contains instructions aimed at the agent | Follow them as part of the task | Treat as untrusted data; flag it; do not let it change the action type or target |
| Approval expired before execution | Execute anyway because it was approved earlier | Mark expired and require a fresh approval of the current payload |
| Target state changed since the proposal | Overwrite using last-write-wins | Let the conditional mutation fail on the precondition; re-propose from current state |
Tenant-safe identity
If you let clients or agents supply idempotency or operation keys, scope every uniqueness constraint and lookup by tenant. A global table keyed only by the supplied key lets one tenant's key collide with, or replay the stored response of, another tenant. Put tenant ID in the primary key, in the hash envelope, in the downstream reference where the target allows it and in row-level security. Test with two tenants that deliberately use the same operation ID.
Identifiers and digests can be sensitive
- Do not build operation IDs from personal data. Stripe's own guidance advises against using sensitive data such as email addresses or personal identifiers as idempotency keys. Random UUIDs avoid the problem and also avoid collisions between logically different actions.
- A plain hash of a low-entropy payload can be guessed. If the digest appears in logs, chat messages or third-party systems, an attacker who can enumerate likely payloads can confirm a guess. Use a keyed digest (HMAC) with a managed secret when the hash leaves your trust boundary.
- Logs are a copy. Log operation ID, state and hash. Do not log payload bodies in agent traces unless your retention policy and access controls explicitly cover them.
Privacy ownership
Assign a named owner for each store this design creates: the payload store, the approval audit trail, the outbox and the agent trace system. For each, decide retention, access roles and how a deletion request propagates. The audit trail wants immutability; deletion rights may want removal. A common resolution is to keep minimal metadata (operation ID, state, timestamps, approver ID, hash) and delete or crypto-shred the payload on schedule or on request. Whether a digest still counts as personal data for your situation is a legal question for your counsel. Our verified erasure workflow design covers how to prove deletion across stores. Nothing here is legal advice, and obligations differ by jurisdiction, contract and data type.
Why This Fails in the Real World
Key takeaway: The failures are rarely about one careless engineer. They come from layers that each behave reasonably and combine badly: a retry layer, a UI layer and a model layer that nobody treats as one system.
The four scenarios below are illustrative composites built from common failure modes, not reports of specific customers or measured results.
Scenario 1: The approved summary and the executed arguments diverge
An agent drafts a credit for a customer and writes a clear explanation. The reviewer approves the explanation. At execution time, the orchestrator re-invokes the planner to "finalize the tool call", and the model, now with slightly different context, emits a different amount. Everything worked as built. The approval never covered the arguments.
Why capable teams still miss it: the approval flow was designed as a UX feature, and the execution flow was designed as a tool-calling feature. No one owned the contract between them. Fix: store the arguments at proposal time, render the approval from them and execute only from them.
Scenario 2: A timeout triggers a second side effect
A worker sends a request to a downstream billing or provisioning API and the connection drops. The agent framework's retry layer generates a fresh key for each attempt, so the target sees two distinct operations. Both succeed.
Why it happens: retry middleware is configured for availability, and idempotency is assumed to be the target's problem. Fix: the operation identity is generated once and persisted before dispatch; a timeout moves the operation to outcome_unknown, and the next step is a lookup by reference, not a resend. Read our piece on reconcile-before-retry for the full reasoning.
Scenario 3: Cross-tenant key collision
A shared dedupe table is keyed by a client-supplied request key. Two tenants' integrations both use a counter-based key. The second tenant's request returns the first tenant's stored response, which contains another tenant's data.
Why it happens: the table was built for a single-tenant prototype and multi-tenancy was added at the API layer only. Fix: tenant ID in the primary key and hash envelope, and a two-tenant collision test in CI.
Scenario 4: Acknowledged, but not recorded
The approval button in a chat tool calls your endpoint. The handler returns success immediately and enqueues work in memory. The pod restarts. The reviewer sees a confirmation, and nothing ever executes. A week later someone notices.
Why it happens: fast acknowledgment is rewarded because chat platforms retry slow endpoints. Fix: commit approval, state change and outbox row in one transaction, acknowledge after commit and add a reconciliation job that alerts on approved operations that never reach a terminal state.
The common thread
In each case the weak point is an implicit assumption between components. Making the assumption an explicit state, constraint or test is cheaper than debugging it at 3 a.m. For a broader treatment of agent-system failure modes, see our engineering playbook for reliability in distributed AI agent systems.
Go/No-Go Controls and a Lower-Risk Rollout
Key takeaway: Treat the controls below as all-required gates for any consequential autonomous action. A missing control is a no-go for that action, not a point deduction. Passing them does not certify the system as production ready; it means these specific risks are covered.
Decision artifact: mandatory go/no-go controls
| # | Control | Evidence to collect | No-go if |
|---|---|---|---|
| 1 | Approval binds to payload hash | Test: change one field after approval; execution is refused | Execution can read arguments from anywhere except the approved record |
| 2 | Reviewer sees the hashed fields | UI renders from stored payload; review of rendering code | UI shows model-written prose in place of structured fields |
| 3 | Durable, tenant-scoped operation identity | Primary key includes tenant; two-tenant collision test passes | Any uniqueness check is global or in memory |
| 4 | Changed-payload key reuse rejected | Test: same ID, different hash returns conflict | The second payload overwrites or is silently ignored |
| 5 | Atomic claim | Concurrency test with many workers; one claim per operation | Claim is read-then-write without a conditional update |
| 6 | Approval, state and scheduling recorded atomically | Crash test between steps; no lost or phantom work | Acknowledgment can precede durable commit |
| 7 | Terminal failure defined | List of definitive rejection codes per target | Any timeout or 5xx is treated as definitive failure |
| 8 | Unknown state and reconcile path | Target supports lookup by reference; reconcile job tested | The only recovery is blind retry |
| 9 | Target-side conditional mutation or disclosed limitation | Documented precondition or idempotency contract, or a recorded risk acceptance | Correctness depends on a client-side check then write |
| 10 | Uncertain input blocks approval | Test with ambiguous fixtures yields needs_clarification | Agent can fill required fields by guessing |
| 11 | Expiry and re-approval | Test executes after expiry; operation is expired, not run | Approvals never expire |
| 12 | Privacy owner and retention defined | Named owners for payload store, audit trail, logs | Payload bodies sit in logs or traces with no retention policy |
| 13 | Downstream authorization (complete mediation) | Target enforces policy on the acting user's scope | The agent's service account has broad rights and authorization is only in the prompt |
A lower-risk rollout path
- Shadow mode. The agent creates proposals and the system stores and hashes them, but nothing executes. Compare proposals with what humans actually did.
- Human-executed with proposal assist. Reviewers execute through your normal tools, using the stored payload as a pre-fill. You learn which fields are unreliable.
- Autonomous for one narrow, reversible action. Choose a target with a lookup-by-reference and a conditional mutation. Keep rate limits and monitoring on, as OWASP suggests for limiting damage.
- Expand by action type, not by agent. Each new action type must pass the go/no-go table on its own target contract.
What to do by role
- Tech lead: own the state machine and the tests for controls 1 to 9.
- Security engineer: own controls 2, 3, 12 and 13, and the prompt-injection test fixtures.
- Product or operations owner: own expiry windows, reviewer experience and the escalation path for outcome_unknown.
Is your agent about to execute something nobody approved?
If approvals, retries and tenant boundaries were designed by different teams, the gaps sit between them. A design review before launch is cheaper than an incident review after it.
Talk to Developers.dev engineers about approval-gated agent execution, reconciliation and tenant-safe design.
Contact UsConclusion: Decisions to Make Before Your Agent Takes a Consequential Action
An agent that can act is a distributed system with a probabilistic planner at the front. The planner will occasionally be wrong, so the guarantees have to come from the parts you control. Five concrete actions:
- Classify your actions by consequence and reversibility. Reserve the hash-bound design for the ones you cannot cheaply undo.
- Persist the proposal and its operation identity before asking for approval. Make the payload and hash immutable, and execute only from the approved record.
- Replace read-then-write with conditional updates for claims in your datastore and, wherever the target allows, for the mutation itself. Where it does not, record the limitation and reduce autonomy.
- Add an explicit outcome_unknown state with a reconcile job, and ban blind retries for consequential mutations.
- Name owners for tenant isolation tests and for retention of payloads, digests and traces before launch, and confirm legal obligations with your own counsel.
Remember that the design here is a proposal for you to challenge. Test it against your target systems' real API contracts before you rely on it.
About the team behind this guide
Developers.dev has delivered software engineering and staff augmentation services since 2007, with delivery teams in India serving clients mainly in the USA, EMEA and Australia. Company materials list CMMI Level 5, ISO 27001 and SOC 2 among our credentials; the scope of any certification applies to defined organizational boundaries, so confirm what covers your engagement. This article was reviewed by the Developers.dev Expert Team.
Frequently Asked Questions
Why not just ask the user to confirm in chat before the agent calls a tool?
A chat confirmation approves the model's description of an action. The arguments sent to the tool may differ from that description because the model re-plans, summarizes imperfectly or is influenced by injected content. A hash-bound approval ties the human decision to the exact payload that executes. For low-impact, reversible actions, a chat confirmation can be an acceptable trade-off.
Is an idempotency key enough to make agent actions safe?
No. An idempotency key helps a target deduplicate retries, but it does not prove that a human approved the payload, that the payload is unchanged, or that the target actually honors the key in the way you assume. You still need payload binding, tenant-scoped identity, atomic claims and a reconcile path. Always check the target's own documented contract; for example, Stripe documents specific behavior for its API that should not be assumed for other services.
What should happen when a request times out?
Record the operation as outcome unknown, which means completion could not be confirmed. Then look up the authoritative state by the operation reference. Only if the target authoritatively shows the effect did not occur, and the approval is still valid, should the same operation identity be re-dispatched. Never generate a new key and retry blindly for a consequential mutation.
How do I distinguish a retry from a legitimate repeat action?
- Same tenant, operation ID and payload hash: a retry. Return the recorded state.
- Same operation ID, different hash: a conflict. Reject it.
- New operation ID with similar content: a new operation. Show the reviewer related recent operations and enforce hard business limits in the target or policy layer.
Does the IETF Idempotency-Key header solve this problem?
It addresses one slice: how an HTTP client can signal that a request should be treated idempotently. It is scoped to HTTP APIs, and you should verify the current status of the specification and whether your target implements it. It does not cover human approval, queue-based workers, database claims or agent planning.
Can I store a hash of the payload in logs safely?
Not always. A plain hash of a low-entropy payload can be confirmed by guessing likely inputs. If the hash leaves your trust boundary, use a keyed digest with a managed secret, restrict log access and define retention. Whether a digest counts as personal data in your situation is a legal question for your counsel.
What if the target system cannot be queried by reference?
Then you have no authoritative way to resolve an unknown outcome automatically. Treat that as a no-go for autonomous execution of consequential actions against that target. Options include adding a reference field and lookup endpoint, serializing actions per resource, or keeping execution human-performed with the stored proposal as an assist.
Ship agent actions you can explain to your auditor and your on-call engineer
Approval binding, tenant-safe operation identity and reconcile-first recovery are easier to build before launch than to retrofit after a duplicate side effect.
Explore our AI agent development services and talk to engineers who build and review these systems.
Contact UsAI Agent Development Services
This guide is designed for engineering leaders who want to plan a practical implementation. Use the related Developers.dev path to compare delivery options, implementation fit, risk, and practical next steps.
Reviewed by the Experts team
This guide is reviewed for clarity, technical and operational relevance, service alignment, and a useful next step. Verified by our SEO team for clear search presentation.
Reviewed by the Experts team. Verified by our SEO team. Validate legal, security, data, budget, and operational requirements with the relevant stakeholders before rollout.

