Skip to content

n8n Human in the Loop with a Durable Approval API

Build an n8n human-in-the-loop approval that survives restarts, fails closed, and records the result after your workflow continues.

Human approval request connected to an automated workflow

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:

Architecture Flow
1 Create Action with request_id 2 Show amount and reason 3 Wait for up to 30 seconds 4 Current authoritative state 5 Issue refund with request_id 6 Report success or failure Stop without refunding n8n workflow Actionbox Reviewer Refund API API- n8n workflow Actionbox Reviewer Refund API 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:

json
{
  "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:

SettingValue
MethodPOST
URLhttps://api.actionbox.cloud/v1/actions
AuthenticationHeader credential containing Authorization: Bearer <Source key>
HeaderIdempotency-Key: n8n:refund:{{$json.request_id}}
Body content typeJSON

Use this request body:

json
{
  "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:

plaintext
GET https://api.actionbox.cloud/v1/source/actions/{data.id}?wait_seconds=30

Use 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 stateNext step
openLoop back to Read approval
resolved with response.type = boolean and response.value = trueContinue to the refund node
resolved with any other responseStop as rejected
expired or cancelledStop 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:

plaintext
POST https://api.actionbox.cloud/v1/actions/{data.id}/outcome

For a successful refund, send:

json
{
  "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?

FailureSafe behavior
Create request times outRetry with the same idempotency key
Actionbox cannot be reachedStop before the refund node
Reviewer rejectsEnd the workflow without refunding
Approval expiresEnd the workflow without refunding
n8n restarts while waitingRe-run create with the same key, then continue polling
Refund API fails after approvalReport a failed outcome; do not rewrite the decision
Outcome report failsRetry 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.

Create a free Source

Turn the next risky operation into a reviewable decision.

Create a free Source, run the example from this guide, and keep the decision and execution outcome connected.