Automation has one fatal flaw: it never hesitates. Your AI agent doesn't feel a twinge of doubt before emailing a client or deleting a row — it just does it. The fix isn't slowing the agent down. It's giving it a supervisor: human-in-the-loop approval, the checkpoint where a person says yes or no before the machine acts.
n8n has built-in human-in-the-loop: a workflow pauses at a checkpoint, sends an approval request through a channel you choose (Telegram, Slack, Gmail, or the n8n Chat), and resumes only when a person taps Approve or Deny. Since n8n 2.6, you can also attach this review directly to an AI Agent's tools — the agent shows exactly what parameters it wants to use, and the tool only executes on approval.
The details that separate a working checkpoint from a hung workflow: timeout handling (section 8), capturing who approved (section 6), and teaching the agent how to behave after a denial (section 4). The full importable template is in section 9.
On this page
- Which Actions Need Approval
- The 3 Ways to Do HITL in n8n
- Native #1: Human Review on AI Agent Tools
- Teaching the Agent to Handle Denials
- Native #2: The Chat Node (Send & Wait)
- Native #3: Slack Approvals (Full Setup)
- Telegram Approvals (Full Setup)
- Timeouts & Auto-Decisions
- Free: Two-Stage Approval Template
- 8 Common Errors + Fixes
- FAQ
- Sources & References
1. Which Actions Need Approval
Approval checkpoints slow things down — add them where the cost of a mistake outweighs the cost of waiting. The rule of thumb that has survived every agent project I've run:
Apply the same lens to non-agent workflows. A nightly backup doesn't need a human. A workflow that deletes inactive users — that one does.
2. The 3 Ways to Do HITL in n8n
n8n gives you three approaches, from native to DIY. Know all three — each earns its place:
| Approach | How it works | Best for |
|---|---|---|
| 1. Human review on AI Agent tools (n8n 2.6+) | Built-in review step attached to the agent's tool connector | Gating specific high-risk agent tools — the default for new agent builds |
| 2. Chat node — Send & Wait | Workflow pauses; user replies in the n8n Chat with buttons or text | Chat-based agents; both workflow-level and tool-level checkpoints |
| 3. Two-stage workflows | Workflow A sends the request and ends; Workflow B reacts to the answer | Long wait windows (hours/days) — no hung executions |
The honest guidance: approaches 1 and 2 are easier and native — use them first. Approach 3 (section 9) is the escape hatch for when approvals must survive days, because a workflow paused for days is an execution waiting to time out, get pruned, or tie up a queue-mode worker.
3. Native #1: Human Review on AI Agent Tools
Since n8n 2.6 (January 2026), the cleanest approval in the whole platform is built into the AI Agent itself. Here's the flow:
Open the agent's Tools connector
On your AI Agent node, click the + on the Tools connector and scroll to the Human review section.
Pick the approval channel
Select Telegram, Slack, Gmail, or Chat — and configure it with the relevant credentials. This is where the approval requests will arrive.
Attach the tools that need oversight
Connect each high-risk tool to the review step's tool connector and configure it normally. From now on: when the agent wants that tool, the workflow pauses, the reviewer sees which tool and with what parameters, and the tool runs only after Approve.
On denial, the tool call is canceled and the AI is informed of the rejection — which is exactly where the next section begins.
4. Teaching the Agent to Handle Denials
The most overlooked part of HITL is the prompt. A denied agent with no instructions does the dumbest possible thing: it retries the same tool, gets denied again, and loops until your token budget cries. Your system prompt needs explicit denial behavior:
If an approval for a tool is denied, do NOT retry that tool.
Instead, inform the user of the rejection, suggest an alternative,
or ask the user for clarification — whichever fits the situation.
Never loop on a denied tool call.
Also describe which tools require approval and why — an agent that knows "sending an email needs a human" asks cleaner questions and sets better user expectations.
5. Native #2: The Chat Node (Send & Wait)
The n8n Chat has two human-in-the-loop operations, and they fit both positions in a workflow:
- Send a message — notify the user and continue; no pause.
- Send a message and wait for response — pause execution until the user replies, with free text or inline approval buttons you define in the node.
Two placements, matching the two philosophies:
| Placement | How | What it gates |
|---|---|---|
| Workflow-level | Add the Chat node to the main connector after any node | A broad checkpoint anywhere in the flow — "human, is this output good enough to continue?" |
| Tool-level | Add it via the AI Agent's Tools connector | One specific tool call — the agent pauses before that single action |
Setup detail that trips people up: for the Chat node to pause and reply mid-workflow, the Chat Trigger must run with Response Mode: Using Response Nodes — set in the trigger's parameters. Without it, the chat answers once and the wait never happens.
6. Slack Approvals (Full Setup)
Slack is the favorite approval channel for teams — and the only one with real setup requirements. From the official changelog, the complete checklist:
Make n8n reachable over public HTTPS
Slack pushes the approval tap to your server — so your n8n must have a public URL. If you're behind CGNAT, tunnel it (Cloudflare Tunnel) or use Telegram instead.
Enable Interactivity in your Slack app
In api.slack.com: Interactivity & Shortcuts → ON, and set the Request URL to https://your-n8n-domain/webhook-waiting-slack — that exact path.
Paste the signing secret into the credential
Copy your Slack app's Signing Secret into the Signature Secret field of the Slack credential in n8n. Skip this and every approval tap arrives unsigned — and gets rejected.
Configure the node
On the Slack node: Response Type → Approval, then in Advanced Interactivity enable Capture Who Responded — the output then records the approver's ID, name, username, channel, message ID, and email (when scopes allow).
Restrict who may approve
List allowed user IDs in Restrict Who Can Approve. Everyone else sees the request; only listed users can act. For refunds and deployments, this is the whole point.
7. Telegram Approvals (Full Setup)
Telegram approvals work the same way but with one advantage: no public HTTPS requirement — your bot's webhook setup already handled that, or the polling community node avoids it entirely (both covered in our Telegram guide).
- Operation: choose Send and Wait for Response on the Telegram node, with approval buttons in the reply markup.
- One-tap approvals: the approver taps Approve/Deny inside Telegram itself — no links, no forms.
- Capture Who Responded: enable it in the same Advanced Interactivity section as Slack — the output records who tapped, their username, chat, and message ID.
- Restrict Who Can Approve: list allowed Telegram user IDs; everyone else's taps are ignored.
If your approval needs to reach a single person, DM them directly. If a team should share the load, route requests to a private group — the approvers get one shared queue, and Capture Who Responded tells you which of them acted.
8. Timeouts & Auto-Decisions
Every approval request will, one day, go unanswered. Design for it before it happens:
- Short waits (minutes–hours): native review steps hold the execution. Acceptable while your execution timeouts allow it.
- Long waits (days): a paused workflow is a hostage — it occupies an execution slot and can hit the execution timeout. Use the two-stage pattern (section 9): the requesting workflow ends after sending; a second workflow reacts whenever the answer arrives. Nothing hangs.
- Auto-decisions: decide your policy in advance — auto-approve when the risk is low and the delay is worse than the mistake; auto-reject when the action is irreversible. Write it down, because at 2 AM you'll be tempted to improvise.
9. Free Template: Two-Stage Telegram Approval
The two-stage pattern from section 8, implemented: Workflow A sends the approval request with Approve/Reject buttons and finishes; Workflow B listens for the tap and executes or cancels. No hung executions, works on every n8n version, and every node type below matches verified exports.
Workflow A — Request Approval
{
"name": "Approval Requester",
"nodes": [
{
"parameters": {},
"id": "manual",
"name": "When Tested / Triggered",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"jsCode": "// Replace with the action your workflow is proposing\nconst action = 'Publish post #42 to LinkedIn';\nconst chatId = 'YOUR_TELEGRAM_CHAT_ID'; // personal ID or private group\n\nconst payload = {\n chat_id: chatId,\n text: `🔍 Approval needed:\\n\\n${action}\\n\\nApprove or reject:`,\n reply_markup: {\n inline_keyboard: [[\n { text: '✅ Approve', callback_data: 'approve_42' },\n { text: '❌ Reject', callback_data: 'reject_42' }\n ]]\n }\n};\n\nreturn [{ json: { body: JSON.stringify(payload) } }];"
},
"id": "build",
"name": "Build Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [450, 300]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.body }}",
"options": {}
},
"id": "send",
"name": "Send Request (Bot API)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [650, 300]
}
],
"connections": {
"When Tested / Triggered": {
"main": [
[
{
"node": "Build Request",
"type": "main",
"index": 0
}
]
]
},
"Build Request": {
"main": [
[
{
"node": "Send Request (Bot API)",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true
}
}
Workflow B — Handle the Answer
{
"name": "Approval Handler",
"nodes": [
{
"parameters": {
"updates": ["callback_query"],
"additionalFields": {
"download": true
}
},
"id": "trigger",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [250, 300],
"credentials": {
"telegramApi": {
"id": "YOUR_TELEGRAM_CREDENTIAL_ID"
}
}
},
{
"parameters": {
"jsCode": "const item = $input.item.json;\nconst cq = item.callback_query;\nconst data = cq.data; // 'approve_42' | 'reject_42'\nconst approved = data.startsWith('approve');\nconst actionId = data.split('_')[1] || data;\n\nreturn [{\n json: {\n approved,\n actionId,\n callbackQueryId: cq.id,\n chatId: cq.message.chat.id,\n approverName: cq.from.first_name || 'Unknown'\n }\n}];"
},
"id": "parse",
"name": "Parse Tap",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [450, 300]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/answerCallbackQuery",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\"callback_query_id\": \"{{ $json.callbackQueryId }}\", \"text\": \"Thanks!\"}",
"options": {}
},
"id": "answer",
"name": "Close Spinner",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [650, 300]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.approved }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "if-approved",
"name": "Approved?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [850, 300]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\"chat_id\": \"{{ $('Parse Tap').item.json.chatId }}\", \"text\": \"✅ Approved by {{ $('Parse Tap').item.json.approverName }} — action #{{ $('Parse Tap').item.json.actionId }} is running.\"}",
"options": {}
},
"id": "notify-approve",
"name": "Notify Approved",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1050, 200]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\"chat_id\": \"{{ $('Parse Tap').item.json.chatId }}\", \"text\": \"❌ Rejected by {{ $('Parse Tap').item.json.approverName }} — action #{{ $('Parse Tap').item.json.actionId }} cancelled.\"}",
"options": {}
},
"id": "notify-reject",
"name": "Notify Rejected",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1050, 400]
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Parse Tap",
"type": "main",
"index": 0
}
]
]
},
"Parse Tap": {
"main": [
[
{
"node": "Close Spinner",
"type": "main",
"index": 0
}
]
]
},
"Close Spinner": {
"main": [
[
{
"node": "Approved?",
"type": "main",
"index": 0
}
]
]
},
"Approved?": {
"main": [
[
{
"node": "Notify Approved",
"type": "main",
"index": 0
}
],
[
{
"node": "Notify Rejected",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true
}
}
- Where your real action goes: replace the "Notify Approved" message with the actual execution — publish the post, run the refund, delete the rows. The two notify nodes are the placeholders for your approve/reject branches.
- One token, one env var: both workflows read
TELEGRAM_TOKENfrom the environment — nothing sensitive sits in the JSON (same pattern as our Telegram guide). - Scale it up: put the action ID and the actual payload into the callback data, or store pending actions in a sheet/database and look them up on approval — the two-stage structure stays the same.
10. The 8 Most Common Errors & Fixes
| # | Error / Symptom | Cause | Fix |
|---|---|---|---|
| 1 | Workflow hangs for days waiting for approval | No timeout, no escape — the execution just waits | Use the two-stage pattern for long windows, or define an explicit auto-decision policy (section 8) |
| 2 | Approval buttons do nothing / spinner spins forever | The callback isn't answered | Call answerCallbackQuery (the template's "Close Spinner" node) before anything else |
| 3 | Agent executes the tool without any approval | Tool not attached to the human review step, or n8n version older than 2.6 | Attach the tool to the review connector; update n8n — tool review shipped in 2.6 |
| 4 | Slack approvals never arrive | Interactivity off, wrong Request URL, or missing signing secret | The full 5-step Slack checklist in section 6 — including the /webhook-waiting-slack path |
| 5 | Denied action still executes | Branching on the wrong field after the review node | Read the approved flag from the review output (e.g., $json.data.approved), not from memory |
| 6 | Can't tell who approved what | Capture Who Responded not enabled | Enable it in Advanced Interactivity (Slack/Telegram) — the output carries ID, name, username, and message ID |
| 7 | Agent loops: asks approval, gets denied, asks again | System prompt has no denial behavior | Add explicit denial instructions to the prompt (section 4) |
| 8 | Chat approvals never pause the chat | Chat Trigger isn't in Response Mode: Using Response Nodes | Set the trigger's response mode, then the Chat node's wait operation works (section 5) |
11. Frequently Asked Questions
Human-in-the-loop is a built-in n8n feature that pauses a workflow at a checkpoint and waits for a person to approve or deny before continuing. The workflow sends a notification to a channel you configure — Telegram, Slack, Gmail, or the n8n Chat — and halts until the reviewer responds (sections 2–7).
Open the AI Agent node, click the + on the Tools connector, scroll to Human Review, and select your approval channel. Attach the tools that need oversight to the review step. The agent then pauses and shows the reviewer exactly what parameters it wants to pass before the tool runs (section 3).
Telegram, Slack, Gmail, and the n8n Chat interface. Slack and Telegram support single-tap in-app approval buttons and can record who approved, when, and from where. Slack requires your n8n instance to be reachable over public HTTPS (sections 6–7).
Yes. On Slack and Telegram, enable Capture Who Responded in the Advanced Interactivity settings of the Send and Wait for Response operation. The node output records the approver's ID, name, username, channel, and message ID — and for Slack, their email when scopes allow.
Yes. List allowed user IDs in the Restrict Who Can Approve setting. Everyone else sees the request but cannot act on it — useful when only managers should approve refunds or deployments.
The tool call is canceled and the AI is informed of the rejection. The agent's behavior after denial depends on your system prompt — tell it to inform the user, suggest alternatives, or ask for clarification instead of retrying the same tool in a loop (section 4).
That depends on your design. The native review step waits until a response arrives — which can hang a workflow for days. For long wait windows, use the two-stage pattern (two short workflows instead of one long-lived execution) or configure a timeout with an explicit auto-approve or auto-reject policy (section 8).
Yes. Your n8n instance must be reachable from Slack over public HTTPS. Turn on Interactivity in your Slack app, set the Request URL to your n8n address with /webhook-waiting-slack, and paste your Slack signing secret into the Slack credential (section 6).
Tool-level approval gates one specific tool call — the agent must get a thumbs-up before that tool executes. Workflow-level approval pauses the entire workflow at a checkpoint. Use tool-level for high-risk agent tools; workflow-level for broad checkpoints anywhere in the flow (section 5).
Yes. When a reviewer denies, the tool call is canceled and the rejection is passed back to the agent. The system prompt decides what happens next — so write explicit denial behavior: inform the user, offer alternatives, or ask for clarification (section 4).
Sources & References
- n8n Documentation — Human-in-the-loop for tools
- n8n Documentation — Changelog (approval features & Chat node operations)
- n8n Documentation — AI Agent nodes
- n8n Community — How the community handles HITL steps
- n8n Documentation — Wait node
