An n8n human-in-the-loop workflow should pause before the risky node, create one durable approval request, and continue only after an explicit decision. Give every logical request a stable idempotency key, set a server-side expiry, treat rejection and timeout as terminal, and report whether the approved operation actually succeeded.
n8n already supports human review for AI tool calls. That is the simplest choice when its built-in Chat, Slack, Teams, Telegram, Gmail, Outlook, WhatsApp, or Discord channels fit your workflow.
Use an external approval API when the decision needs to sit in the same shared inbox as requests from other systems, or when your application needs a durable record that is independent of one n8n execution.
The workflow we are building
This example puts a refund approval between an automated check and the refund API:
The Actionbox Source key can create, read, cancel, and report outcomes for its own Actions. Keep it in an n8n credential. Do not paste it into a Code node, workflow export, URL, or expression.
1. Start with a stable input
The workflow needs an identifier that means "this exact refund request," even after a retry. A database row ID, payment ID, or job ID works. An n8n execution ID alone is weaker because a manual restart can produce a new execution for the same business operation.
Use an input shaped like this:
{
"request_id": "refund_8472",
"customer": "Acme Corp",
"amount": "425.00 USD",
"reason": "Duplicate invoice",
"approval_expires_at": "2026-09-01T22:00:00Z"
}Pass only the context a reviewer needs. Payment credentials, full customer records, access tokens, and hidden model prompts do not belong in an approval request.
2. Create the Action with an HTTP Request node
Configure an n8n HTTP Request node named Create approval:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://api.actionbox.cloud/v1/actions |
| Authentication | Header credential containing Authorization: Bearer <Source key> |
| Header | Idempotency-Key: n8n:refund:{{$json.request_id}} |
| Body content type | JSON |
Use this request body:
{
"title": "Approve refund {{$json.request_id}}?",
"description": "Review this refund before n8n calls the payment provider.",
"priority": "high",
"expires_at": "{{$json.approval_expires_at}}",
"interaction": {
"type": "boolean",
"label": "Issue this refund?",
"true_label": "Approve refund",
"false_label": "Reject"
},
"context": [
{
"type": "key_value",
"title": "Refund request",
"items": {
"Request": "{{$json.request_id}}",
"Customer": "{{$json.customer}}",
"Amount": "{{$json.amount}}",
"Reason": "{{$json.reason}}"
}
}
],
"metadata": {
"origin": {
"provider": "automation",
"ref": "n8n refund {{$json.request_id}}"
},
"request_id": "{{$json.request_id}}"
}
}Actionbox returns the Action in data. Keep data.id, data.action_version, and data.fingerprint available to later nodes. The same idempotency key returns the same logical Action if n8n is unsure whether the first request succeeded.
Do not reuse the key for a later, genuinely different refund. Idempotency prevents duplicate requests; it is not a general cache key.
3. Wait without holding one connection forever
Add another HTTP Request node named Read approval:
GET https://api.actionbox.cloud/v1/source/actions/{data.id}?wait_seconds=30Use the same Source credential. The request waits for a state change for up to 30 seconds, then returns the current Action. This is a bounded long poll, not an hour-long HTTP request.
After the read, use a Switch node:
| Action state | Next step |
|---|---|
open | Loop back to Read approval |
resolved with response.type = boolean and response.value = true | Continue to the refund node |
resolved with any other response | Stop as rejected |
expired or cancelled | Stop without changing money |
The expires_at value prevents the loop from waiting forever. Actionbox makes an expired Action terminal. Your workflow should not invent an approval because a timer, network request, or credential check failed.
n8n's Wait node is still useful for longer backoff intervals or a separate webhook design. The source read above already provides a bounded wait, so an extra Wait node is optional here.
4. Put the side effect after the decision
Only the approved Switch branch should reach the refund API. Give that API the same request_id as its idempotency key if the provider supports one.
This protects two different boundaries:
- The Actionbox key prevents duplicate human requests.
- The refund provider key prevents duplicate money movement after retries.
An approval says the operation may run. It does not prove that the operation ran once, or that it succeeded.
5. Report the execution outcome
After the refund node, call:
POST https://api.actionbox.cloud/v1/actions/{data.id}/outcomeFor a successful refund, send:
{
"status": "success",
"duration_ms": 1840,
"rollback": false,
"reason_code": null,
"action_version": 1,
"fingerprint": "the fingerprint returned by Actionbox"
}Connect the refund node's error output to a second outcome request with status: "failed" and a bounded reason_code, such as provider_rejected. Do not put raw provider errors or customer data in the reason code.
Use the Action's actual action_version and fingerprint, not the sample values above. They bind the outcome to the exact decision the reviewer saw.
What happens when something goes wrong?
| Failure | Safe behavior |
|---|---|
| Create request times out | Retry with the same idempotency key |
| Actionbox cannot be reached | Stop before the refund node |
| Reviewer rejects | End the workflow without refunding |
| Approval expires | End the workflow without refunding |
| n8n restarts while waiting | Re-run create with the same key, then continue polling |
| Refund API fails after approval | Report a failed outcome; do not rewrite the decision |
| Outcome report fails | Retry the same outcome request |
If an operator manually stops the workflow while its Action is still open, call POST /v1/actions/{id}/cancel from your n8n error workflow when possible. That keeps an abandoned request from looking actionable.
Native n8n review or Actionbox?
Choose native n8n human review when the approval belongs to an AI Agent tool call and one of n8n's supported channels is the natural place to answer.
Choose Actionbox when you need a cross-system decision inbox, Source-scoped API access, a typed decision that another service can re-read, or an execution outcome attached to the original approval.
Both approaches keep a person in the loop. The important part is placing the gate before the irreversible node and defining every failure as a non-approval.
Ship the first workflow
Create a Source, store its one-time key in n8n credentials, and test the approve, reject, expiry, restart, and downstream failure paths before connecting a real payment provider.
