I’ve been building AI agent workflows in n8n and wanted to actually test how far the native “Require approval” feature goes versus a custom-built approval gate. Sharing what I found, plus the working template.
**What n8n’s native HITL does well:** it’s genuinely simple to turn on — check a box on a tool, pick a Slack channel, done. For a single agent with a handful of sensitive tools, it works exactly as advertised.
**Where it stops:** no decision history (can’t answer “what did we approve last week”), and it only covers AI Agent tool calls specifically — nothing else in a workflow.
So I built a general-purpose version: any process can request approval via Slack + email fallback, and — the part I actually wanted — every request gets logged to a sheet the moment it’s raised, before anyone answers. Approved, denied, or still pending, there’s a record.
**The template (free):**
```json
{
“name”: “Autonomous Ops Kit - Human Approval Gate”,
“nodes”: [
{
“parameters”: {
“httpMethod”: “POST”,
“path”: “approval-request”,
“responseMode”: “responseNode”,
“options”: {}
},
“id”: “1”,
“name”: “Decision Requested (Webhook)”,
“type”: “n8n-nodes-base.webhook”,
“typeVersion”: 2,
“position”: [240, 300]
},
{
“parameters”: {
“assignments”: {
“assignments”: [
{ “id”: “a1”, “name”: “decision_id”, “type”: “string”, “value”: “={{ $json.decision_id || $now.toMillis() }}” },
{ “id”: “a2”, “name”: “title”, “type”: “string”, “value”: “={{ $json.title }}” },
{ “id”: “a3”, “name”: “summary”, “type”: “string”, “value”: “={{ $json.summary }}” },
{ “id”: “a4”, “name”: “requested_at”, “type”: “string”, “value”: “={{ $now.toISO() }}” },
{ “id”: “a5”, “name”: “status”, “type”: “string”, “value”: “pending” }
]
}
},
“id”: “2”,
“name”: “Normalize Request”,
“type”: “n8n-nodes-base.set”,
“typeVersion”: 3.4,
“position”: [460, 300]
},
{
“parameters”: {
“resource”: “message”,
“operation”: “post”,
“select”: “channel”,
“channelId”: “={{ $env.APPROVALS_SLACK_CHANNEL }}”,
“text”: “=*Approval needed:* {{$json.title}}\n{{$json.summary}}\n\nReply *approve {{$json.decision_id}}* or *deny {{$json.decision_id}}* in this channel.”,
“otherOptions”: {}
},
“id”: “3”,
“name”: “Notify Human (Slack)”,
“type”: “n8n-nodes-base.slack”,
“typeVersion”: 2.2,
“position”: [680, 200],
“notes”: “Swap for Email/Telegram node if not using Slack.”
},
{
“parameters”: {
“sendTo”: “={{ $env.APPROVALS_EMAIL }}”,
“subject”: “=Approval needed: {{$json.title}}”,
“message”: “={{$json.summary}}\n\nDecision ID: {{$json.decision_id}}\nReply APPROVE or DENY plus the Decision ID.”,
“options”: {}
},
“id”: “4”,
“name”: “Notify Human (Email fallback)”,
“type”: “n8n-nodes-base.emailSend”,
“typeVersion”: 2.1,
“position”: [680, 400],
“notes”: “Optional redundant channel — disable if Slack-only.”
},
{
“parameters”: {
“operation”: “append”,
“sheetId”: “={{ $env.DECISION_LOG_SHEET_ID }}”,
“range”: “Decisions!A:F”,
“options”: {}
},
“id”: “5”,
“name”: “Log Decision (Pending)”,
“type”: “n8n-nodes-base.googleSheets”,
“typeVersion”: 4.5,
“position”: [900, 300],
“notes”: “Every request is logged the moment it’s raised, before any response.”
},
{
“parameters”: {
“respondWith”: “json”,
“responseBody”: “={{ { \“status\”: \“pending\”, \“decision_id\”: $json.decision_id } }}”
},
“id”: “6”,
“name”: “Acknowledge Request”,
“type”: “n8n-nodes-base.respondToWebhook”,
“typeVersion”: 1.4,
“position”: [1120, 300]
}
],
“connections”: {
“Decision Requested (Webhook)”: { “main”: [[{ “node”: “Normalize Request”, “type”: “main”, “index”: 0 }]] },
“Normalize Request”: {
“main”: [[
{ “node”: “Notify Human (Slack)”, “type”: “main”, “index”: 0 },
{ “node”: “Notify Human (Email fallback)”, “type”: “main”, “index”: 0 }
]]
},
“Notify Human (Slack)”: { “main”: [[{ “node”: “Log Decision (Pending)”, “type”: “main”, “index”: 0 }]] },
“Notify Human (Email fallback)”: { “main”: [[{ “node”: “Log Decision (Pending)”, “type”: “main”, “index”: 0 }]] },
“Log Decision (Pending)”: { “main”: [[{ “node”: “Acknowledge Request”, “type”: “main”, “index”: 0 }]] }
}
}
```
Tested this against real webhook triggers before posting — works cleanly on both n8n Cloud and self-hosted.
If this is useful, I’ve got two more in the same pattern (monthly expense reconciliation against your actual bank statement, and a content-review gate for AI-drafted publishing) bundled together here: Autonomous Ops Kit — 3 n8n Templates for AI-Agent Businesses
Happy to answer questions about the node setup in the comments.