An AI agent drafts a customer refund for $480. You approve it in Slack. Then the workflow retries after a timeout, asks the model to reconstruct the request, and sends $840 instead. The human clicked approve, but nobody proved that the executed action was the one that person saw.
That is the gap most n8n approval tutorials skip. n8n's human-review feature can pause an AI Agent before selected tools run, and the reviewer can inspect the proposed parameters. That helps. It does not automatically protect the surrounding workflow from changed arguments, duplicate sends, or stale approvals. Build the approval around a frozen action record, not a promise that the model will repeat itself.
The practical design: let the agent prepare and explain an action, but keep execution deterministic. Save the proposed tool name and arguments once, show that stored record to the approver, and let a non-agent step execute only that record after approval.
Freeze the request before approval
Start with the action you might regret: a payment, external email, public post, account change, or destructive database write. Let the agent produce a structured proposal. Do not connect the irreversible operation directly to whatever arguments the model supplies after someone clicks a button.
n8n's native Human review option attaches to tools connected to an AI Agent. The reviewer sees the tool name and parameters, then approves or denies. The docs expose $tool.name and $tool.parameters for building that review message. Useful, but your workflow still owns the record of what was approved.
A sturdier layout has four jobs: prepare, store, approve, execute. The prepare step creates a record with approval_id, execution_id, action_type, arguments, payload_hash, status, expires_at, and approved_by. Store the exact argument object before asking for approval. Compute a stable hash from a canonical serialization of the action type and arguments. Sort object keys before hashing if order can vary. Do not hash a display message that includes mutable text.
Show a human enough detail to catch a mistake. For a refund, include customer ID, amount, currency, and reason. For email, show recipient, subject, and the complete message or a link to a versioned draft. A card saying “Approve refund?” is not a useful control.
When the decision arrives, load the saved proposal by approval_id. Check that it is pending, not expired, and decided by an authorized approver. Compare its payload hash with the hash carried by the approval request. Only then should a deterministic node call the business API using saved arguments. The model does not get another chance to rewrite them.
This is where agentic CI's safety gates connect to ordinary automation: separate the agent's ability to propose from the system's authority to perform. The approval record is the boundary.
Make retries and timeouts fail closed
Retries can turn a decent approval flow into a dangerous one if they replay the final action. Give each proposed action a unique idempotency key, based on the stored approval ID and action version. Enforce uniqueness in the database or target API. A check-then-write sequence across separate steps has a race window: two retries can both observe “not sent” and both proceed. Prefer an atomic insert with a unique constraint or an API that accepts an idempotency key.
Treat approval as a one-use state transition: pending becomes approved, rejected, or expired, but only once. The executor should atomically claim an approved record before sending. After success, store the provider's result and mark it executed. If the request times out after the provider accepted it, reconcile by idempotency key before retrying. Do not blindly resend because the first response was missing.
For n8n's Wait node, webhook-resume mode fits a human decision that may arrive later. It generates a $execution.resumeUrl unique to that execution and supports Basic, Header, or JWT authentication. Do not expose an unauthenticated resume URL as if it were a harmless approval button. Keep the URL out of public logs and analytics, and pass a signed or opaque decision reference instead of trusting arbitrary query parameters.
One easy-to-miss detail: n8n documents that waits shorter than 65 seconds keep running in process instead of offloading execution data to the database. A human approval can outlast that window, so use webhook resume and test the behavior you intend to operate. For long-running workflows, verify database persistence and backups too.
Set an expiry and decide what happens when nobody responds. For money, customer communication, or destructive writes, the safe default is deny or route to a human queue, never silently approve. A timeout should update the approval record and stop the execution path. If a late approval arrives, the status and expiry checks should reject it.
Test the failure paths, not only the happy one. Submit a proposal, inspect the stored payload and hash, approve it, then replay the callback twice. The target should show one effect. Change one argument after approval and verify the executor refuses it. Let the request expire and verify a late click does nothing. Finally, force a timeout after the external service accepts the first request. A retry should retrieve the prior result rather than create a second effect.
That is the difference between a button-shaped pause and a real control. n8n gives you pause and resume machinery. Your workflow has to preserve the human's decision through the final write.
Sources
- n8n human review for AI tools: review flow, visible tool parameters, and channels
- n8n Wait node documentation: resume URLs, authentication, and the 65 seconds persistence detail
- n8n Community discussion on approval retries: duplicate proposals and idempotency keys
