When a workflow requires human approval (e.g., content review, expense approval, data verification), use the **Wait** node to pause execution until the human...
When a workflow requires human approval (e.g., content review, expense approval, data verification), use the Wait node to pause execution until the human responds. Always configure a timeout so the workflow does not hang indefinitely if the approver is unavailable.
Real-world example: A content publishing workflow generates an article with AI, then waits for editor approval before publishing. If no response comes within 48 hours, it escalates to the managing editor.
Workflow structure:
[Trigger] --> [AI: Generate Article] --> [Slack: Send for Review]
--> [Wait: 48h timeout] --> [IF: Approved?]
|
+--------+--------+
| |
[Approved] [Timeout/Rejected]
| |
[Publish] [Escalate to Manager]
```text
Wait node configuration:
```json
{
"resume": "webhook",
"options": {
"webhookSuffix": "article-review",
"timeout": "48",
"timeoutUnit": "hours"
}
}
```text
The Slack message includes approval buttons that call back to the Wait node's resume URL:
```javascript
// Code node: "Prepare Approval Request"
const article = $input.first().json;
const resumeUrl = $execution.resumeUrl;
return [{
json: {
slackMessage: {
channel: '#content-review',
text: `*New article for review*\n\nTitle: ${article.title}\nWord count: ${article.wordCount}\nGenerated by: ${article.model}`,
attachments: [
{
text: article.summary,
fallback: 'Article review request',
callback_id: 'article_review',
actions: [
{
type: 'button',
text: 'Approve',
style: 'primary',
url: `${resumeUrl}?approved=true`
},
{
type: 'button',
text: 'Reject',
style: 'danger',
url: `${resumeUrl}?approved=false&reason=needs_revision`
}
]
}
]
},
article
}
}];
```text
Handle the timeout case:
```javascript
// Code node: "Handle Wait Result" (after the Wait node)
const waitResult = $input.first().json;
// Check if this was a timeout or a response
if (waitResult.$timedOut) {
return [{
json: {
status: 'timeout',
action: 'escalate',
message: 'No response received within 48 hours'
}
}];
}
return [{
json: {
status: waitResult.query?.approved === 'true' ? 'approved' : 'rejected',
reason: waitResult.query?.reason || null,
respondedAt: new Date().toISOString()
}
}];
```text
> **Warning: Wait node and execution data**
>
> When a workflow resumes from a Wait node, n8n loads the execution data from the database. On self-hosted instances, make sure your database has enough storage and that execution data pruning does not delete waiting executions. Set `EXECUTIONS_DATA_MAX_AGE` higher than your longest Wait timeout.
This pattern enables human oversight without blocking indefinitely. The timeout ensures every workflow eventually reaches a terminal state.
**Related:** [Use "Pin Data" to Freeze Node Output](../testing-and-debugging/01-use-pin-data-to-freeze-node-output.md) | [Break Large Workflows into Sub-Workflows](../workflow-architecture/01-break-large-workflows-into-sub-workflows.md)
I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.