Human speed meets machine scale. Use n8n human in the loop to keep automation fast without flying blind
A fully automated flow moves fast. A safe flow knows when to pause and ask
During Apollo 11 the autopilot got the Eagle close. Neil Armstrong took manual control to avoid a boulder field. That mix of machine speed and human judgment saved the mission
Why HITL in n8n
What you’ll learn: When to add human‑in‑the‑loop (HITL), where risk hides, and how approvals improve accuracy and compliance
Use human gates when automation crosses real risk. Add approvals for clarity and control
High‑stakes and compliance
- Approvals for spend, refunds, data deletes, or permission changes
- Sign‑off where regulation or audit applies
- Recordkeeping to capture who decided and why
Pause before irreversible actions to avoid expensive rollbacks
AI output review
- Human review of AI drafts for tone, facts, and brand fit
- Safety gates before agents email customers or change data
- Feedback capture to improve prompts and policies
A tiny review loop prevents big reputation damage
Edge cases rules miss
- Low‑confidence outliers routed to people
- Cross‑system conflicts surfaced before they cascade
- Safe fallbacks chosen by humans when rules do not apply
Rules cover the usual. People catch the weird
Transition: With the why in place, let’s set up the core pieces you will reuse everywhere
Core building blocks
What you’ll learn: How to pause with Wait, resume via webhook, use \$execution.resumeUrl, and pick decision channels
Wait with webhook resume
- Insert a Wait node where the pause belongs
- Set Resume When to On Webhook Call or On Form Submitted
- Persist state so n8n sleeps until you resume
Path: Workflow - Wait - Resume When: On Webhook Call
Outputs: $execution.resumeUrl available to downstream nodes
Short pauses save long firefights
flowchart TD
T[Trigger] --> P[Prepare Data]
P --> W[Wait Node]
W -->|Resume| R[Resume Flow]
R --> A[Act on Decision]
classDef trigger fill:#e1f5fe,stroke:#01579b
classDef process fill:#fff3e0,stroke:#ef6c00
classDef action fill:#e8f5e8,stroke:#2e7d32
class T trigger
class P process
class W process
class R process
class A action
Using \$execution.resumeUrl safely
- One‑click links that carry a decision
- Do not expose raw resume URLs to public channels
- Prefer a frontend or static webhook that validates then resumes
Approve: [Approve]({{$execution.resumeUrl}}?decision=approve)
Reject: [Reject]({{$execution.resumeUrl}}?decision=reject)
Wrap with one‑time tokens or map IDs server side for extra safety
Decision channels
Pick the path users will actually click, then layer guardrails
| Channel | Best for | Tradeoffs |
|---|---|---|
| Wait Form | Rich inputs | Built‑in fields, but requires opening a page |
| Email links | External approvers | Ubiquitous links, but slower loops |
| Slack buttons | Internal teams | Fast UX, but app setup and scopes |
Default to where decisions already happen
flowchart TD
U[User Click] --> V[Validate Token]
V -->|OK| C[Call Resume URL]
C --> D[Continue Flow]
V -->|Fail| E[Deny Request]
classDef process fill:#fff3e0,stroke:#ef6c00
classDef action fill:#e8f5e8,stroke:#2e7d32
classDef alert fill:#f3e5f5,stroke:#7b1fa2
class U process
class V process
class C action
class D action
class E alert
Transition: With the primitives ready, let’s apply them to email and Slack approvals
Email and Slack
What you’ll learn: How to build email approvals, a Gmail send‑and‑wait pattern, and Slack interactive buttons
Email links + Wait
- Build two links that hit the resume URL
- Include context with a short summary above the fold
- Parse query params into a clean decision record
Subject: Approval needed - Invoice {{ $json.invoiceNumber }}
Please review and choose:
- Approve - [Open]({{$execution.resumeUrl}}?decision=approve&by={{$json.managerEmail}})
- Reject - [Open]({{$execution.resumeUrl}}?decision=reject&by={{$json.managerEmail}})
Gotchas
- Email clients can break buttons, include plain links
- User replies instead of clicking, add a monitored mailbox
- Avoid sensitive data in the email body
Email wins reach. Links beat fancy markup
flowchart TD
S[Send Email] --> U[User Click]
U --> H[Webhook Validate]
H -->|Approve| A[Approve Path]
H -->|Reject| R[Reject Path]
classDef process fill:#fff3e0,stroke:#ef6c00
classDef action fill:#e8f5e8,stroke:#2e7d32
class S process
class U process
class H process
class A action
class R action
Gmail send‑and‑wait
- Send Email then Wait with On Webhook Call
- Roll your own links for consistent behavior
- Store decision and metadata before continuing
Custom beats magic when reliability matters
Slack interactive buttons
- Post a Block Kit message with Approve and Reject (Block Kit is Slack’s message layout system)
- Point interactivity to a fixed n8n Webhook node
- Validate payload then call the internal resume URL
{
"blocks": [
{"type":"section","text":{"type":"mrkdwn","text":"Approve email to *{{ $json.customer }}*?"}},
{"type":"actions","elements":[
{"type":"button","text":{"type":"plain_text","text":"Approve"},"style":"primary","value":"approve|{{ $json.requestId }}"},
{"type":"button","text":{"type":"plain_text","text":"Reject"},"style":"danger","value":"reject|{{ $json.requestId }}"}
]}
]
}
flowchart TD
M[Post Slack Msg] --> I[User Click]
I --> W[Webhook Validate]
W -->|OK| R[Resume Execution]
W -->|Fail| X[Notify Error]
classDef trigger fill:#e1f5fe,stroke:#01579b
classDef process fill:#fff3e0,stroke:#ef6c00
classDef action fill:#e8f5e8,stroke:#2e7d32
classDef alert fill:#f3e5f5,stroke:#7b1fa2
class M trigger
class I process
class W process
class R action
class X alert
Keep resume URLs server side to reduce exposure
Transition: Next, let’s stitch the pieces into one end‑to‑end pattern for AI‑assisted outreach
AI to Slack approval
What you’ll learn: How to draft with AI, pause for approval in Slack, and ship only after audit logging
Trigger and AI draft
- Trigger when CRM status changes to Ready for outreach (CRM is your customer relationship system)
- Fetch context such as product, plan, last touch, and contact
- AI node draft with guardrails and style
Add an auto‑critic step that scores tone and factuality before humans see it
Pause and notify in Slack
- Wait node set to On Webhook Call with 48h max
- Post Slack message with the draft and buttons
- Track mapping from requestId to executionId in a datastore
Short waits drive action. Long waits stall revenue
Approve path
- Webhook validates Slack user and requestId
- Resume the execution with
decision=approve - Send email via SMTP or provider API (SMTP is the standard email protocol)
{
"approved": true,
"approved_by": "[email protected]",
"approved_at": "{{$now}}",
"request_id": "{{$json.requestId}}"
}
After approval
- Upsert audit_log row
- Tag the CRM record with sent_by and template_id
- Notify the requester with a link to the thread
Ship the message only after the log writes successfully
Reject path
- Resume with
decision=rejectand a reason - Create a task to revise with the reviewer comment
- Optionally regenerate with updated constraints
A tight loop teaches the model and the team
flowchart TD
C[CRM Trigger] --> D[AI Draft]
D --> Q[Quality Check]
Q --> W[Wait for Approval]
W -->|Approve| S[Send Email]
W -->|Reject| T[Create Task]
classDef trigger fill:#e1f5fe,stroke:#01579b
classDef process fill:#fff3e0,stroke:#ef6c00
classDef action fill:#e8f5e8,stroke:#2e7d32
class C trigger
class D process
class Q process
class W process
class S action
class T action
erDiagram
ApprovalAudit ||--o{ Execution : logs
ApprovalAudit {
int id
string request_id
string workflow_id
string execution_id
string subject
string decision
string decided_by
datetime decided_at
string ip
string reason
string hash
}
Execution {
string id
string workflow_id
}
Transition: Finally, design for the boring day and the worst day with ops and security
Ops and security
What you’ll learn: Timeouts, reminders, multi‑approver patterns, audit logging, and link security
Timeouts and escalations
- Set Wait limits that match business urgency
- Send reminders at 24h and 36h with direct links
- Escalate at 48h to a backup approver
Wait: On Webhook Call
Max wait: 48h
On timeout: route - Escalate - Notify - Close as no‑decision
Timeouts unblock queues. Escalations protect outcomes
Multiple approvers
- Sequential chain Wait nodes in role order
- Parallel fork, collect decisions, then Merge with AND
- Quorum stop when approvals reach a threshold
for approver in approvers:
wait_for_decision(approver)
if decision == reject: abort()
Pick the cheapest path to certainty
Audit logging
- Immutable facts in a dedicated table
- Capture who, what, when, where, and why
- Link records back to source objects for traceability
create table approval_audit (
id uuid primary key,
request_id text,
workflow_id text,
execution_id text,
subject text,
decision text,
decided_by text,
decided_at timestamptz,
ip text,
reason text,
hash text
);
If it is not logged then it did not happen
Security essentials
- Never expose raw resume URLs to the open internet
- Sign links with one‑time tokens and short TTLs (TTL is time to live)
- Re‑fetch source data post‑approval before acting
// Generate one‑time token
const token = hmacSHA256(requestId, env.SECRET).toString()
// Validate at the webhook before resuming
Trust but verify on every resume
Human gates for AI agents
- Gate actions where AI touches customers or money
- Feed feedback into prompts and policies
- Track model OKRs that include human quality scores (OKRs are objectives and key results)
n8n human in the loop keeps agents useful and sane
Design once. Reuse everywhere. Start with one approval, then templatize links, logs, and tokens across flows