Tips > Reliability & Performance

Use Wait Nodes with Timeouts for Human-in-the-Loop Steps

When a workflow requires human approval (e.g., content review, expense approval, data verification), use the **Wait** node to pause execution until the human...

TipAdvanced2 min read

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)

Want this running in your stack?

I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.