<figure class="wp-block-image aligncenter"><img src="https://flowgenius.in/wp-content/uploads/2026/02/designing-human-in-the-loop-n8n.png" alt="Step by Step Guide to solve designing human in the loop n8n" /> <figcaption style="text-align: center;">Step by Step Guide to solve designing human in the loop n8n</p>
<hr />
</figcaption></figure>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Who this is for:</strong> Developers and ops engineers who need a reliable way to pause an automated n8n flow for a manual decision without building a custom UI from scratch. <strong>We cover this in detail in the </strong>n8n Architectural Decision Making Guide.</p>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">One‑Sentence Answer (Featured Snippet)</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">Build a <strong>Webhook → Function → Human‑Approval (Webhook/UI) → Continue</strong> chain, store the decision in a database, and use <strong>Set</strong> and <strong>IF</strong> nodes to branch; add a <strong>Wait</strong> node with reminder and escalation to avoid stalled runs.</p>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>In production, this usually shows up when a request sits in the queue for hours waiting for a sign‑off.</em></p>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">Quick Diagnosis – What problem does this page solve?</h2>
<p><strong>If you encounter any </strong><a href="/n8n-with-custom-api">n8n with custom api </a><strong>resolve them before continuing with the setup.</strong></p>
<p style="margin-bottom: 2em; line-height: 1.9;">Your automation requires a manual sign‑off (e.g., purchase approval) but you don’t want the whole workflow to stop or to write a bespoke front‑end.</p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Solution in 4 steps</strong></p>
<ul style="margin-bottom: 1.5em; line-height: 1.9;">
<li><strong>Webhook</strong> – receives the request that needs approval.</li>
<li><strong>Notification</strong> – send an Approve/Reject button via Slack, Email or a simple UI.</li>
<li><strong>Persist</strong> – write the human’s choice (and metadata) to a DB for auditability.</li>
<li><strong>Branch</strong> – an <strong>IF</strong> node routes the flow to “approved” or “rejected” paths, with timeout‑driven escalation if nobody replies.</li>
</ul>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">1. When to Use a Human‑in‑the‑Loop (HITL) Pattern?</h2>
<p><strong>If you encounter any </strong><a href="/n8n-webhooks-for-developers">n8n webhooks for developers </a><strong>resolve them before continuing with the setup.</strong></p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Use‑case</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Why HITL is required</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Typical n8n nodes</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Purchase order approval</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Legal/compliance check</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Webhook → Slack → IF</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Content moderation</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Subjective judgement</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Webhook → UI → PostgreSQL</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Incident triage</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Prioritisation by on‑call engineer</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Webhook → Email → Set</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Data quality validation</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Humans spot anomalies that scripts miss</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Webhook → Function → IF</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Feature‑flag rollout</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Controlled rollout with a manual gate</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Webhook → Set → HTTP Request</td>
</tr>
</tbody>
</table>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;">
<p style="margin-bottom: 0; line-height: 1.9;"><strong>EEFA note:</strong> Never rely on a single person for critical compliance steps without an immutable audit trail (timestamp, user, payload).</p>
</blockquote>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">2. Prerequisites & Environment Setup</h2>
<p><strong>If you encounter any </strong><a href="/n8n-for-internal-tools-vs-core-systems">n8n for internal tools vs core systems </a><strong>resolve them before continuing with the setup.</strong></p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Item</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Minimum requirement</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">How to verify</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">n8n version</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>≥ 1.2.0</strong> (supports “Webhook Response” UI)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>n8n --version</code></td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Database</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">SQLite (dev) or PostgreSQL/MySQL (prod)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Test connection in **Credentials**</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Notification channel</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Slack, Microsoft Teams, or SMTP</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Install respective node credentials</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Execution mode</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Queue</strong> (for reliability)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Settings → Execution Mode</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">SSL</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Valid certificate for public webhooks</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Use Let’s Encrypt or a reverse‑proxy</td>
</tr>
</tbody>
</table>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;">
<p style="margin-bottom: 0; line-height: 1.9;"><strong>EEFA:</strong> If you run n8n behind a self‑signed cert, add it to **Trusted Certificates**; otherwise the webhook returns <em>“Invalid SSL certificate”</em> and the HITL flow stalls.</p>
</blockquote>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">3. Core Workflow – From Trigger to Human Approval</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Micro‑summary:</strong> The diagram below shows the end‑to‑end data flow, from the incoming request to the final approved/rejected branch.</p>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;">
<p style="margin-bottom: 0; line-height: 1.9;"><em>Most teams run into this after a few weeks, not on day one – the first few approvals feel smooth, then edge‑cases appear.</em></p>
</blockquote>
<div style="margin: 36px auto; max-width: 1280px; height: 720px; display: flex; align-items: center; justify-content: center; font-family: Arial, sans-serif; background: #fafafa; border: 2px solid #e0e0e0; border-radius: 14px; box-sizing: border-box;">
<div style="width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 34px;">
<div style="border: 3px solid #555; padding: 18px 44px; font-size: 18px; background: #fff;">Webhook Trigger</div>
<div style="height: 42px; border-left: 3px solid #555;"></div>
<div style="border: 3px solid #555; padding: 18px 44px; font-size: 18px; background: #fff;">Extract Data (Function)</div>
<div style="height: 42px; border-left: 3px solid #555;"></div>
<div style="border: 3px solid #555; padding: 18px 44px; font-size: 18px; background: #fff;">Notify Approver (Slack / Email)</div>
<div style="height: 42px; border-left: 3px solid #555;"></div>
<div style="border: 4px solid #000; padding: 22px 54px; font-size: 22px; font-weight: bold; background: #fff;">Webhook Approval</div>
</div>
</div>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.1 Trigger & Data Extraction</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"parameters": {
"path": "purchase-request",
"method": "POST",
"responseMode": "onReceived"
},
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [250, 300]
}</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Purpose:</strong> Accept a POST containing the purchase request payload.</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">// Function: Extract only the fields we need
return [{
json: {
requestId: $json.id,
amount: $json.amount,
requester: $json.user
}
}];</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Purpose:</strong> Normalise the incoming JSON so downstream nodes only see the three fields we care about.</p>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;">
<p style="margin-bottom: 0; line-height: 1.9;">*It’s easy to miss this step the first time you wire the flow – you’ll end up with a giant payload that later nodes can’t parse.*</p>
</blockquote>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.2 Sending the Approval Prompt</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"parameters": {
"url": "https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX",
"method": "POST",
"jsonParameters": true,
"options": { "bodyContentType": "json" },
"bodyParametersJson": {
"text": "*Purchase Request*\\nID: {{ $json.requestId }}\\nAmount: ${{ $json.amount }}\\nRequester: {{ $json.requester }}",
"attachments": [{
"fallback": "Approve or reject",
"callback_id": "purchase_approval",
"actions": [
{ "name": "approve", "text": "Approve", "type": "button", "value": "approved" },
{ "name": "reject", "text": "Reject", "type": "button", "value": "rejected" }
]
}]
}
},
"name": "Notify Approver (Slack)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [750, 300]
}</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Purpose:</strong> Post a message with interactive buttons to Slack (or replace with Email/Teams).</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.3 Capturing the Human Decision</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"parameters": {
"path": "approval-response",
"method": "POST",
"responseMode": "onReceived",
"responseCode": 200,
"responseHeaders": [{ "name": "Content-Type", "value": "application/json" }],
"responseBody": "{\"status\":\"received\"}"
},
"name": "Webhook Approval",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [250, 600]
}</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Purpose:</strong> Slack posts the button click to this endpoint.</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">// Function: Pull decision, approver name, and timestamp
return [{
json: {
requestId: $json.requestId,
decision: $json.payload.actions[0].value,
approver: $json.payload.user.name,
timestamp: new Date().toISOString()
}
}];</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Purpose:</strong> Transform the Slack payload into a flat record ready for storage.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.4 Persisting the Decision</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"parameters": {
"operation": "insert",
"table": "approvals",
"columns": ["request_id","decision","approver","timestamp"]
},
"name": "Save Decision",
"type": "n8n-nodes-base.postgres",
"typeVersion": 1,
"position": [750, 600],
"credentials": { "postgres": "PostgresDB" }
}</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Purpose:</strong> Write an immutable audit row; the DB schema is described in Section 5.</p>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;">
<p style="margin-bottom: 0; line-height: 1.9;">*At this point, persisting the decision is usually faster than trying to recompute it later.*</p>
</blockquote>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.5 Branching on the Result</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"parameters": {
"conditions": {
"boolean": [{
"value1": "={{ $json.decision }}",
"operation": "equal",
"value2": "approved"
}]
}
},
"name": "IF Approved",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [1000, 600]
}</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Purpose:</strong> If the decision is **approved**, the left branch fires; otherwise the right branch handles rejection.</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">// Left branch – mark as approved
return [{ json: { status: "Approved", requestId: $json.requestId } }];</pre>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">// Right branch – mark as rejected
return [{ json: { status: "Rejected", requestId: $json.requestId } }];</pre>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">4. Adding Timeout & Escalation Logic</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Micro‑summary:</strong> A <strong>Wait</strong> node followed by conditional checks ensures the workflow never hangs indefinitely.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Scenario</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Nodes to add</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Key configuration</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">No response within 12 h</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">**Wait** → **IF** → **Send Reminder**</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Wait `12h`; IF checks DB for missing decision; reminder via Slack/Email</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Still silent after 24 h</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">**Set** → **Terminate** (status *failed*)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Set `escalated = true`; Terminate with error code for monitoring</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Auto‑reject after timeout</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">**IF** (decision missing) → **Set decision = rejected** → **Save Decision**</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Guarantees an audit row even when nobody answered</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>EEFA tip:</strong> Add a <code>deadline_at</code> column (see Section 5). After the **Wait** node finishes, compare <code>NOW()</code> with <code>deadline_at</code> to decide whether a late response is acceptable.</p>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">5. Persisting State & Auditing</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">Table schema (PostgreSQL)</h3>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Column</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Type</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">id</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">SERIAL PK</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Unique row identifier</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">request_id</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">TEXT</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Business‑level request identifier</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">decision</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">TEXT</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">`approved`, `rejected`, or `timeout`</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">approver</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">TEXT</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Slack/Email user who acted</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">timestamp</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">TIMESTAMPTZ</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Exact moment of decision</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">deadline_at</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">TIMESTAMPTZ</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Configured timeout deadline</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">notes</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">TEXT</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Optional free‑form comment</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Why store in a DB?</strong></p>
<ul style="margin-bottom: 1.5em; line-height: 1.9;">
<li><strong>Immutability</strong> – required for compliance audits.</li>
<li><strong>Replayability</strong> – you can rebuild downstream state from the table.</li>
<li><strong>Metrics</strong> – average approval time, missed deadlines, etc.</li>
</ul>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">6. Testing & Debugging Checklist</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">✅ Item</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">How to verify</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Webhook URL reachable from the internet</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>curl -X POST <url></code> returns **200**</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Slack button payload maps to <code>payload.actions[0].value</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Look at execution data of **Webhook Approval**</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">DB row inserted with correct <code>request_id</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>SELECT * FROM approvals WHERE request_id = 'XYZ';</code></td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">IF node correctly branches on <code>approved</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Run a test approval, watch **Mark as Approved** fire</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Timeout triggers reminder</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Set **Wait** to 1 min, leave request idle, verify reminder message</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Audit trail contains <code>deadline_at</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Confirm column populated on insert</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Workflow fails gracefully on DB loss</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Stop DB, trigger request, ensure **Terminate** fires with error</td>
</tr>
</tbody>
</table>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;">
<p style="margin-bottom: 0; line-height: 1.9;"><strong>EEFA note:</strong> In production, enable **“Save Execution Data → Full”** for at least 30 days. This gives a complete replay window for forensic analysis.</p>
</blockquote>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">7. Common Errors & Production‑Ready Fixes</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Error message</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Likely cause</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">`Error: Request failed with status code 400` (Slack)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Payload exceeds Slack’s 3 000‑char limit</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Trim the message or move large data to a link (e.g., S3)</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">`Webhook not found`</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">URL changed after n8n restart</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Use **Static URL** in Settings or store the URL in an environment variable</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">`duplicate key value violates unique constraint “approvals_request_id_key”`</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Double‑click or retry inserts same row</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Add a **UNIQUE** constraint on <code>request_id</code>; in n8n, check existence before insert</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">`Execution timed out`</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Wait node too short for a human response</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Increase **Execution Timeout** in Settings (default 30 min)</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">`Invalid SSL certificate`</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Self‑signed cert on webhook endpoint</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Add cert to **Trusted Certificates** or terminate TLS at a reverse proxy with a valid cert</td>
</tr>
</tbody>
</table>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">8. Advanced Patterns</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">8.1 Sequential Multiple Approvers</h3>
<ol style="margin-bottom: 1.5em; line-height: 1.9;">
<li>After the first approval, an <strong>IF</strong> node checks <code>decision === "approved"</code> and triggers a second <strong>Webhook → Slack</strong> pair for the next approver.</li>
<li>Store each step in the same <code>approvals</code> table with a <code>stage</code> column (e.g., <code>stage = 1</code>, <code>stage = 2</code>).</li>
</ol>
<h3 style="margin-bottom: 45px; line-height: 1.3;">8.2 Dynamic Forms</h3>
<ul style="margin-bottom: 1.5em; line-height: 1.9;">
<li>Use a **Function** node to build a JSON payload that includes extra fields based on request type.</li>
<li>Render those fields with Slack **Block Kit** or Microsoft Teams **Adaptive Cards**.</li>
</ul>
<h3 style="margin-bottom: 45px; line-height: 1.3;">8.3 External Form Services (Typeform, Google Forms)</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">Replace the Slack button with a pre‑filled Typeform link. Capture the response via the **Typeform Webhook** node, then reuse the **Parse Approval** logic unchanged.</p>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">9. Deploying to Production</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Step</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; text-align: left;">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Environment variables</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Store secrets (<code>N8N_WEBHOOK_URL</code>, <code>POSTGRES_HOST</code>, <code>SLACK_WEBHOOK</code>) in <code>.env</code></td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Docker compose</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Use the official <code>n8n</code> image, mount a persistent volume for SQLite (if used), set <code>EXECUTIONS_PROCESS=main</code></td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Scaling</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Run n8n behind a **Redis Queue** (<code>EXECUTIONS_MODE=queue</code>) and spin up multiple worker containers</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Monitoring</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Enable Prometheus metrics (<code>N8N_METRICS=true</code>) and set alerts on <code>workflow_execution_failed_total</code></td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Backup</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Nightly <code>pg_dump</code> of the <code>approvals</code> table → immutable object store (e.g., S3 Glacier)</td>
</tr>
</tbody>
</table>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;">
<p style="margin-bottom: 0; line-height: 1.9;"><strong>EEFA reminder:</strong> After every deployment, re‑run the timeout & escalation tests. A mis‑configured **Wait** can leave requests hanging, consuming worker slots and driving up costs.</p>
</blockquote>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">9.1 Timeout Flow Diagram</h2>
<div style="margin: 36px auto; max-width: 1280px; height: 720px; display: flex; align-items: center; justify-content: center; font-family: Arial, sans-serif; background: #fafafa; border: 2px solid #e0e0e0; border-radius: 14px; box-sizing: border-box;">
<div style="width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 34px;">
<div style="border: 3px solid #555; padding: 18px 44px; font-size: 18px; background: #fff;">Wait 12 h</div>
<div style="height: 42px; border-left: 3px solid #555;"></div>
<div style="border: 3px solid #555; padding: 18px 44px; font-size: 18px; background: #fff;">Decision found?</div>
<div style="height: 42px; border-left: 3px solid #555;"></div>
<div style="display: flex; gap: 72px; margin-top: 42px;">
<div style="border: 2px solid #777; padding: 16px 28px; font-size: 16px; background: #fff;">Yes → Continue</div>
<div style="border: 2px solid #777; padding: 16px 28px; font-size: 16px; background: #fff;">No → Send Reminder</div>
</div>
<div style="height: 42px; border-left: 3px solid #555;"></div>
<div style="border: 3px solid #555; padding: 18px 44px; font-size: 18px; background: #fff;">Wait 12 h (total 24 h)</div>
<div style="height: 42px; border-left: 3px solid #555;"></div>
<div style="border: 4px solid #000; padding: 22px 54px; font-size: 22px; font-weight: bold; background: #fff;">Escalate / Auto‑reject</div>
</div>
</div>
<hr style="margin: 50px 0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">10. Conclusion</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">A human‑in‑the‑loop workflow in n8n can be built with just a handful of nodes, yet it provides a <strong>production‑grade</strong> audit trail, timeout handling, and clear branching logic. By:</p>
<ol style="margin-bottom: 1.5em; line-height: 1.9;">
<li><strong>Persisting every decision</strong> in a relational table,</li>
<li><strong>Adding a Wait + reminder</strong> to keep the flow alive, and</li>
<li><strong>Deploying with queue mode, SSL, and monitoring</strong>,</li>
</ol>
<p style="margin-bottom: 2em; line-height: 1.9;">you get a fast, reliable automation that still respects the need for manual oversight. Deploy, monitor, and iterate on the timeout thresholds to match your organization’s SLA—your automation will stay both efficient <strong>and</strong> compliant.</p>
Step by Step Guide to solve designing human in the loop n8n
Who this is for: Developers and ops engineers who need a reliable way to pause an automated n8n flow for a manual decision without building a custom UI from scratch. We cover this in detail in the n8n Architectural Decision Making Guide.
One‑Sentence Answer (Featured Snippet)
Build a Webhook → Function → Human‑Approval (Webhook/UI) → Continue chain, store the decision in a database, and use Set and IF nodes to branch; add a Wait node with reminder and escalation to avoid stalled runs.
In production, this usually shows up when a request sits in the queue for hours waiting for a sign‑off.
Quick Diagnosis – What problem does this page solve?
If you encounter any n8n with custom api resolve them before continuing with the setup.
Your automation requires a manual sign‑off (e.g., purchase approval) but you don’t want the whole workflow to stop or to write a bespoke front‑end.
Solution in 4 steps
Webhook – receives the request that needs approval.
Notification – send an Approve/Reject button via Slack, Email or a simple UI.
Persist – write the human’s choice (and metadata) to a DB for auditability.
Branch – an IF node routes the flow to “approved” or “rejected” paths, with timeout‑driven escalation if nobody replies.
1. When to Use a Human‑in‑the‑Loop (HITL) Pattern?
EEFA: If you run n8n behind a self‑signed cert, add it to **Trusted Certificates**; otherwise the webhook returns “Invalid SSL certificate” and the HITL flow stalls.
3. Core Workflow – From Trigger to Human Approval
Micro‑summary: The diagram below shows the end‑to‑end data flow, from the incoming request to the final approved/rejected branch.
Most teams run into this after a few weeks, not on day one – the first few approvals feel smooth, then edge‑cases appear.
EEFA tip: Add a deadline_at column (see Section 5). After the **Wait** node finishes, compare NOW() with deadline_at to decide whether a late response is acceptable.
5. Persisting State & Auditing
Table schema (PostgreSQL)
Column
Type
Description
id
SERIAL PK
Unique row identifier
request_id
TEXT
Business‑level request identifier
decision
TEXT
`approved`, `rejected`, or `timeout`
approver
TEXT
Slack/Email user who acted
timestamp
TIMESTAMPTZ
Exact moment of decision
deadline_at
TIMESTAMPTZ
Configured timeout deadline
notes
TEXT
Optional free‑form comment
Why store in a DB?
Immutability – required for compliance audits.
Replayability – you can rebuild downstream state from the table.
Metrics – average approval time, missed deadlines, etc.
6. Testing & Debugging Checklist
✅ Item
How to verify
Webhook URL reachable from the internet
curl -X POST <url> returns **200**
Slack button payload maps to payload.actions[0].value
Look at execution data of **Webhook Approval**
DB row inserted with correct request_id
SELECT * FROM approvals WHERE request_id = 'XYZ';
IF node correctly branches on approved
Run a test approval, watch **Mark as Approved** fire
Timeout triggers reminder
Set **Wait** to 1 min, leave request idle, verify reminder message
Audit trail contains deadline_at
Confirm column populated on insert
Workflow fails gracefully on DB loss
Stop DB, trigger request, ensure **Terminate** fires with error
EEFA note: In production, enable **“Save Execution Data → Full”** for at least 30 days. This gives a complete replay window for forensic analysis.
7. Common Errors & Production‑Ready Fixes
Error message
Likely cause
Fix
`Error: Request failed with status code 400` (Slack)
Payload exceeds Slack’s 3 000‑char limit
Trim the message or move large data to a link (e.g., S3)
`Webhook not found`
URL changed after n8n restart
Use **Static URL** in Settings or store the URL in an environment variable
`duplicate key value violates unique constraint “approvals_request_id_key”`
Double‑click or retry inserts same row
Add a **UNIQUE** constraint on request_id; in n8n, check existence before insert
`Execution timed out`
Wait node too short for a human response
Increase **Execution Timeout** in Settings (default 30 min)
`Invalid SSL certificate`
Self‑signed cert on webhook endpoint
Add cert to **Trusted Certificates** or terminate TLS at a reverse proxy with a valid cert
8. Advanced Patterns
8.1 Sequential Multiple Approvers
After the first approval, an IF node checks decision === "approved" and triggers a second Webhook → Slack pair for the next approver.
Store each step in the same approvals table with a stage column (e.g., stage = 1, stage = 2).
8.2 Dynamic Forms
Use a **Function** node to build a JSON payload that includes extra fields based on request type.
Render those fields with Slack **Block Kit** or Microsoft Teams **Adaptive Cards**.
8.3 External Form Services (Typeform, Google Forms)
Replace the Slack button with a pre‑filled Typeform link. Capture the response via the **Typeform Webhook** node, then reuse the **Parse Approval** logic unchanged.
9. Deploying to Production
Step
Action
Environment variables
Store secrets (N8N_WEBHOOK_URL, POSTGRES_HOST, SLACK_WEBHOOK) in .env
Docker compose
Use the official n8n image, mount a persistent volume for SQLite (if used), set EXECUTIONS_PROCESS=main
Scaling
Run n8n behind a **Redis Queue** (EXECUTIONS_MODE=queue) and spin up multiple worker containers
Monitoring
Enable Prometheus metrics (N8N_METRICS=true) and set alerts on workflow_execution_failed_total
Backup
Nightly pg_dump of the approvals table → immutable object store (e.g., S3 Glacier)
EEFA reminder: After every deployment, re‑run the timeout & escalation tests. A mis‑configured **Wait** can leave requests hanging, consuming worker slots and driving up costs.
9.1 Timeout Flow Diagram
Wait 12 h
Decision found?
Yes → Continue
No → Send Reminder
Wait 12 h (total 24 h)
Escalate / Auto‑reject
10. Conclusion
A human‑in‑the‑loop workflow in n8n can be built with just a handful of nodes, yet it provides a production‑grade audit trail, timeout handling, and clear branching logic. By:
Persisting every decision in a relational table,
Adding a Wait + reminder to keep the flow alive, and
Deploying with queue mode, SSL, and monitoring,
you get a fast, reliable automation that still respects the need for manual oversight. Deploy, monitor, and iterate on the timeout thresholds to match your organization’s SLA—your automation will stay both efficient and compliant.