Updated on: .
Disclosure: Operant Solo is reader-supported. We may earn an affiliate commission when you purchase through links on this page, at no additional cost to you. Pages that include our own dated test say so and link the test files; other pages are based on vendor documentation and public pricing, which can change.
Quick answer: For n8n human in the loop, pause the workflow at a Wait node (form) or Slack “Send and Wait for Response,” then branch on three separate outcomes: approved, rejected and timeout. Only approved may continue. In our Cloud tests, rejection and timeout both blocked the send. Run a patched n8n version, because approval-gate bugs were fixed in September 2026.
Free workflow included below: a form lab you can run without another account, and a Slack lab for teams, both tested for approve, reject, and timeout.
Your workflow has written an email. Before it sends, you want a person to read the exact draft and choose Approve or Reject. That is the job of an n8n human-in-the-loop step. This guide shows a Wait form and Slack approval, with a free workflow you can copy and real results from both routes.
Which n8n human-in-the-loop node should you use?
- Start with a review form: use the Wait node when you want to try the included lab without connecting another account.
- Ask a teammate in Slack: use Slack Send and Wait for Response. It sends the request and waits in one node.
- Review an AI Agent tool call: use the agent’s Human review connection for the specific tool. The setup differs from the two tested labs below.
Pick the route that matches your task. Then test approval, rejection and no response before adding a real send or update node.
Updated October 8, 2026. The form lab below was tested in n8n Cloud on October 7. The native Slack approval lab was tested in n8n Cloud on October 8: approve, reject and no response all took the expected route. The two-minute expiry test was then restored to 24 hours. These were desktop browser tests with fictional data; no draft was published.
| Tested (Cloud, 7-8 Oct 2026) | Not tested |
|---|---|
| Form: approve, reject, no response (55.4 s shortened timeout) | Full 24-hour timeout run to completion |
| Slack: executions 339 (approve), 340 (reject), 341 (timeout, 2 min 3.9 s) | Phone approval; identity-restricted approval |
| Fictional data, no external actions | Duplicate callbacks and real downstream sends in production |
How to build an n8n human-in-the-loop workflow
- Prepare the draft. Keep its recipient, job ID, revision and expiry time together.
- Request a decision. Use a native Send and Wait for Response node, or a Wait node with a review form.
- Set a deadline. Enable a 24-hour limit so an unanswered request does not wait forever.
- Check the result. Treat approve, reject and timeout as separate outcomes.
- Gate the final action. Only the approved branch can send or update anything. Recheck the draft and prevent duplicate writes first.
Start with the credential-free lab below. It uses a fictional reminder and records a decision without sending email, posting to Slack or calling an AI model.
Try the tested approval form first
The four nodes are Manual Trigger → Approval context → Review draft → Check decision. The context stores a fictional draft and a deadline. The Wait node displays an Approve/Reject form. The final Code node returns a decision and a boolean called sendAllowed.
This is a learning workflow, not a complete client-email automation. It has no sending node, external credentials or identity check for the reviewer. Keep the review link private. Before using it for real work, add appropriate authentication and an authorized-reviewer check.
- Expand the JSON below and copy the whole object.
- Open a blank n8n workflow and paste it onto the canvas, or save it as a .json file and use Import → From file.
- Wait for the imported nodes to finish loading, then execute the workflow.
- Open Review draft. While it is waiting, use the form link shown in the output panel.
- Choose Approve or Reject and submit. Open Check decision to inspect the result.
Copy the complete approval form workflow JSON
{
"name": "OperantSolo approval lab — form, no external actions",
"nodes": [
{
"id": "manual",
"name": "Run approval lab",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"parameters": {}
},
{
"id": "prepare",
"name": "Approval context",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
],
"parameters": {
"jsCode": "return [{json:{jobId:'approval-lab-001',draft:'Fictional reminder: the team workshop starts at 10am.',expiresAt:new Date(Date.now()+24*60*60*1000).toISOString()}}];"
}
},
{
"id": "review",
"name": "Review draft",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
480,
0
],
"webhookId": "operantsolo-approval-lab",
"parameters": {
"resume": "form",
"formTitle": "OperantSolo approval lab",
"formDescription": "Review a fictional workshop reminder. This lab records your choice; it never sends a message or changes a calendar.",
"formFields": {
"values": [
{
"fieldLabel": "Decision",
"fieldType": "dropdown",
"fieldOptions": {
"values": [
{
"option": "Approve"
},
{
"option": "Reject"
}
]
},
"requiredField": true
}
]
},
"limitWaitTime": true,
"limitType": "afterTimeInterval",
"resumeAmount": 24,
"resumeUnit": "hours",
"options": {}
}
},
{
"id": "decision",
"name": "Check decision",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
0
],
"parameters": {
"jsCode": "const job=$('Approval context').first().json;\nconst response=$input.first().json;\nconst choice=response.Decision ?? response.body?.Decision;\nconst expired=Date.now()>=Date.parse(job.expiresAt);\nconst decision=expired || !choice ? 'timeout' : choice==='Approve' ? 'approved' : 'rejected';\nreturn [{json:{jobId:job.jobId,decision,sendAllowed:decision==='approved',externalActions:0,note:'Demo only: no message was sent.'}}];"
}
}
],
"connections": {
"Run approval lab": {
"main": [
[
{
"node": "Approval context",
"type": "main",
"index": 0
}
]
]
},
"Approval context": {
"main": [
[
{
"node": "Review draft",
"type": "main",
"index": 0
}
]
]
},
"Review draft": {
"main": [
[
{
"node": "Check decision",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"active": false
}Each execution gets its own review link. Do not reuse an old execution’s link. For a custom notification before the Wait node, use $execution.resumeFormUrl for a form or $execution.resumeUrl for a webhook. These are generated at runtime; there is no permanent approval URL to copy before the workflow starts.

What the real lab returned
| Test | Observed decision | Allowed to send? |
|---|---|---|
| Approve form | approved | true; no external action exists in this lab |
| Reject form | rejected | false |
| No response | timeout | false |
Approval and rejection were submitted through the actual n8n form. The no-response test used a shorter timeout setting instead of waiting a full day. Its execution completed in 55.432 seconds, including scheduling and execution overhead. The reusable JSON is configured for 24 hours. This test verifies the decision routing; it does not establish a precise timeout service-level guarantee.

Configure n8n Slack approval
For a team already using Slack, start with the native Slack → Message → Send and Wait for Response operation and choose Approval as the response type. It handles the request and pause in one node, so this route does not need a separate Wait node.
Enable both choices: open Approval Options, add Values, then set Type of Approval to Approve and Disapprove. Change the Disapprove Button Label to Reject if you want that wording. The default is Approve Only, which does not show a rejection button.
Use a private test channel first. Include the exact draft, intended recipient and revision in the request. A button labelled Approve should approve a specific action, not a vague summary. Keep the original job in an earlier node called Approval context so you can retrieve it after the response.
The default link-based approval and advanced inline Slack buttons have different setup requirements. If you need verified responder details or restricted approvers, check the current n8n Slack approval instructions. Advanced interactivity requires the Slack app configuration and signing secret described there. Do not assume a forwarded link proves who approved.
What happened in our real Slack test
We connected Slack to an isolated four-node lab: Manual Trigger → Approval context → Slack review → Check decision. The request went to a private channel. We used the standard link-based buttons, so clicking a button opened an n8n page that recorded the decision. Advanced interactivity was off.
- Approve: execution 339 returned
data.approved: true. The decision was approved and sendAllowed was true. - Reject: execution 340 returned
data.approved: false. The decision was rejected and sendAllowed was false. - No response: execution 341 finished in 2 minutes 3.893 seconds with the limit set to two minutes. The response had no approval boolean, so the decision was timeout and sendAllowed was false.
All three runs stopped after recording the result. There was no email, publishing or calendar node. The 24-hour setting below is the restored configuration; we did not wait 24 hours for this test. A physical phone and identity-restricted approval were not tested.



Set the 24-hour deadline
For the included Wait-form workflow, use these exact settings. For native Slack Send and Wait, add its Limit Wait Time option and set the response window to the same 24 hours. Check your node’s output with a short test before connecting a sending action.
| Setting | Value | Purpose |
|---|---|---|
| Resume | On Form Submitted | Wait for the review form |
| Limit Wait Time | On | Enable expiry |
| Limit Type | After Time Interval | Use a relative deadline |
| Amount / Unit | 24 / Hours | Maximum review window |
| After resume | Explicit decision check | An expired request cannot send |
Store a matching expiresAt value when preparing the job. For example, in a Code node use new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(). A deadline in the decision check prevents a late approval from authorizing stale work.
Check approve, reject and timeout separately
A native approval response uses data.approved. Compare it to the boolean true; do not accept the string “true” or the presence of any response as approval. Missing approval data must take the stop/escalation path. The form lab uses its own Decision dropdown, so it has different parsing code.
Decision code for a native approval node
// Single-job example. Code node: Run Once for All Items.
const job = $('Approval context').first().json;
const response = $input.first().json;
const deadline = Date.parse(job.expiresAt);
const expired = !Number.isFinite(deadline) || Date.now() >= deadline;
const approved = response.data?.approved;
let decision = 'timeout';
if (!expired && approved === true) decision = 'approved';
else if (!expired && approved === false) decision = 'rejected';
return [{ json: { ...job, decision, sendAllowed: decision === 'approved' } }];This example processes one job per execution. For multiple items, correlate each response to its job ID instead of taking the first item. Add an IF node with sendAllowed set to is true. Only its true output connects to the final action. Use a Switch on decision for the false path: rejected records the refusal and stops; timeout notifies a backup reviewer and stops.
A useful timeout notice says which job expired, which revision was reviewed and where to open a fresh request. An escalation must never silently approve the old draft. If a backup reviewer approves, validate a new request and its deadline.
Protect the final send from stale or duplicate approvals
Approval is only one check. Immediately before sending, reload the job and confirm that its recipient, content and revision still match what the reviewer saw. If someone edited the message after approval, request another review.
Keep a durable record keyed by job ID and revision. An already-completed job must not send again when a callback is retried. If the downstream service supports an idempotency key, use it. Otherwise design a durable claim/status transition and a recovery check for uncertain sends. A Code node variable alone is not a reliable duplicate-send ledger.
If an API times out after a send, check whether the action succeeded before retrying. Our n8n 429 and Retry-After lab explains why retries also need safety checks. For predictable drafts, see structured outputs in n8n.
Test the paths before connecting real accounts
| Case | Expected result | What to inspect |
|---|---|---|
| Approve valid draft | Approved branch only | Job, revision and final recipient match |
| Reject | Stop | No send/update node runs |
| No response | Timeout and escalation | No automatic approval |
| Approve after expiry | Stop or fresh review | Deadline check fails closed |
| Duplicate callback | No second write | Durable job status or idempotency key |
| Sending API returns uncertain result | Investigate before retry | Check downstream state |
Keep error handling separate from rejection. A credential error or failed notification should alert an operator and stop the sending path. It is not evidence that a reviewer rejected the draft, and it must not count as approval. Use the n8n error workflow guide to plan failure alerts alongside the approval paths.
Patch n8n before you trust an approval gate
An approval step is only as strong as the endpoint that resumes it. n8n published several advisories in September 2026 that affect exactly these gates. Our form and Slack tests did not probe these flaws, so passing those tests does not mean an unpatched instance is safe.
| Advisory | What it affects | Severity | Fixed in |
|---|---|---|---|
| GHSA-597w-c3jh-g8fg | Signed resume URL for Send and Wait could be crafted by someone who can save workflows, producing a valid approve/decline link for a gate in a project they cannot read | High (CVSS 7.0) | 1.123.80, 2.39.6, 2.40.1 |
| GHSA-728h-pmr2-7cgh | Send-and-Wait HMAC check could be skipped, letting a caller holding an execution’s resume token approve it | High (CVSS 7.0) | 2.41.4, 2.42.1 |
| GHSA-p3pg-xw4f-m72c | A project member could answer another member’s pending agent tool approval | Moderate (CVSS 6.1) | 2.41.4, 2.42.0 |
n8n’s 1 October 2026 security update lists v1.123.83, v2.41.4 and v2.42.1 as the fixed versions per branch. Cloud instances are patched automatically; self-hosted instances need the upgrade. Confirm the current numbers in the n8n advisories before upgrading, and see the self-hosted patch and hardening checklist.
Until you are patched, n8n’s advisory workarounds are temporary: limit who can edit workflows, avoid approval nodes guarding high-impact actions behind a public trigger, and review execution history for unexpected approvals. Also keep the checks this tutorial already uses: a deadline in the decision code and approval tied to the exact draft.
Human review for AI Agent tool calls
On an AI Agent node, click the Tools connector, add a Human review step, pick a channel, then attach the tools that need approval. n8n’s docs list Chat, Slack, Discord, Telegram, Microsoft Teams, Gmail, WhatsApp Business Cloud, Google Chat and Microsoft Outlook as channels. A reviewer can approve, so the tool runs, or deny, so the action is cancelled and the agent is told it was rejected. Mention approval in the system prompt so the agent handles a denial sensibly. We did not test these channels; our evidence covers the form and Slack Send-and-Wait paths above.
If an AI Agent selects a tool dynamically, use n8n’s native Human review connection for that tool. This lets a reviewer inspect a proposed tool call before it executes. For a fixed workflow where AI only writes a draft, the approval pattern above is often easier to follow.
Review the actual tool arguments: recipient, record ID, amount or text. Approving “the agent’s plan” is too broad when the next tool can change something important. See the official human review tool guide for the connection setup. For deciding which actions should require a person, read our human oversight design guide.
Common setup mistakes
The review link does not work
Check that the execution is still waiting and that you used the runtime link from that execution. Check authentication and any expiry too. A copied link from yesterday’s run does not approve today’s job.
The workflow sends after nobody responds
The decision check is likely treating any continuation as approval. Inspect the native node’s data.approved field or the form’s Decision field. Connect the sending node only to an explicit approved branch.
Do I need PostgreSQL for a Wait node?
No. PostgreSQL is not a mandatory feature requirement for this pattern. Use a supported, persistent n8n database and storage setup. For self-hosting, maintain backups and test recovery after a restart. Do not rely on an ephemeral database volume.
Does self-hosting keep all the data on my server?
No. Slack, email providers and hosted AI APIs still receive the data sent to them by your nodes. Review those connections and send only the information each step needs.
Can I approve from a phone?
A reviewer can use the review form link in a browser. For Slack or email approval, test the exact client your team uses. The screenshots here show the real browser form, not a verified Slack mobile session.
Build your next approval workflow
Run the safe lab first, inspect all three decisions, then add one real action behind the approved branch. Start with an internal draft and check the resulting record yourself. Browse the n8n workflow hub for more practical labs.
Sources: Wait node documentation, Slack node operations, and n8n’s human oversight playbook. Node options can change between versions; check the labels in your installed version.
n8n
Best for: technical automation workflows
Consider n8n when your workflow needs custom logic or control over deployment. Self-hosting also requires time for updates, backups, and monitoring.
Operant Solo may earn a commission if you purchase through this link, at no extra cost to you.
You can read this article without subscribing. Want the next tested article?

