The `$execution.resumeUrl` is a unique URL generated by the Wait node that, when called, resumes the paused execution.
The $execution.resumeUrl is a unique URL generated by the Wait node that, when called, resumes the paused execution. Use it to build sophisticated approval flows where external systems or users can approve, reject, or provide input -- with full timeout handling so nothing stays stuck forever.
Real-world example: An expense approval workflow sends the manager a Slack message with approve/reject buttons. If no action is taken in 72 hours, it auto-escalates to the VP. If the VP does not respond in 24 hours, it auto-rejects with a notification to the submitter.
Workflow structure:
[Expense Submitted]
--> [Code: Format Request]
--> [Slack: Send to Manager]
--> [Wait: 72h timeout, resume via webhook]
--> [Switch: Response Type]
|
+----+----+----+
| | |
[Approved] [Rejected] [Timeout]
| | |
[Process] [Notify [Escalate: Slack to VP]
Submitter] --> [Wait: 24h timeout]
--> [Switch: VP Response]
|
+----+----+
| |
[Approved] [Timeout]
| |
[Process] [Auto-Reject
+ Notify All]
```text
Building the approval message with the resume URL:
```javascript
// Code node: "Build Manager Approval Request"
// This runs BEFORE the Wait node to prepare the message
// The Wait node generates the resumeUrl when it executes
const expense = $input.first().json;
// Note: $execution.resumeUrl is available inside the Wait node
// or in nodes after it. For the Slack message, we use a
// two-step approach: configure the Wait node first, then
// reference its resume URL in the Slack message.
return [{
json: {
expense,
managerEmail: expense.manager_email,
slackChannel: expense.manager_slack_id,
amount: expense.amount,
currency: expense.currency,
description: expense.description,
submitter: expense.submitter_name
}
}];
```text
Wait node configuration with webhook resume:
```json
{
"resume": "webhook",
"options": {
"webhookSuffix": "/expense-approval",
"responseMode": "lastNode",
"timeout": 72,
"timeoutUnit": "hours"
}
}
```text
Slack message using the resume URL (configured in a Slack node after the Wait node's URL is generated, or using a Code node with `$execution.resumeUrl`):
```javascript
// Code node: "Send Approval Slack Message"
// Use this approach: the Slack message is sent via HTTP Request
// so we can include the resume URL as button links
const resumeUrl = $execution.resumeUrl;
const expense = $json.expense;
const slackPayload = {
channel: expense.manager_slack_id,
text: `Expense approval request from ${expense.submitter_name}`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: [
`*Expense Approval Request*`,
`*From:* ${expense.submitter_name}`,
`*Amount:* ${expense.currency} ${expense.amount}`,
`*Description:* ${expense.description}`,
`*Category:* ${expense.category}`
].join('\n')
}
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'Approve' },
style: 'primary',
url: `${resumeUrl}?decision=approved&approver=manager`
},
{
type: 'button',
text: { type: 'plain_text', text: 'Reject' },
style: 'danger',
url: `${resumeUrl}?decision=rejected&approver=manager`
},
{
type: 'button',
text: { type: 'plain_text', text: 'Need More Info' },
url: `${resumeUrl}?decision=info_needed&approver=manager`
}
]
}
]
};
return [{ json: slackPayload }];
```text
Handle all three outcomes (approved, rejected, timeout):
```javascript
// Code node: "Process Approval Response" (after the Wait node resumes)
const response = $input.first().json;
// Determine if this was a timeout or a human response
if (!response.query || !response.query.decision) {
// Timeout -- no response received
return [{
json: {
outcome: 'timeout',
level: 'manager',
action: 'escalate_to_vp',
message: 'Manager did not respond within 72 hours'
}
}];
}
const decision = response.query.decision;
const approver = response.query.approver;
return [{
json: {
outcome: decision,
approver,
respondedAt: new Date().toISOString(),
action: decision === 'approved' ? 'process_expense'
: decision === 'rejected' ? 'notify_submitter'
: 'request_more_info'
}
}];
```text
```yaml
Timeout chain summary:
Step 1: Manager gets Slack message
--> 72h to respond
--> Timeout: escalate to VP
Step 2: VP gets Slack message
--> 24h to respond
--> Timeout: auto-reject
Step 3: Submitter gets notification of final outcome
--> Always reached (no infinite hangs)
--> Includes who approved/rejected/timed out
```text
> **Note: Resume URL security**
>
> The `$execution.resumeUrl` contains a unique token that identifies the specific execution. Anyone with this URL can resume the workflow. For sensitive approval flows, add a verification step after resume that checks the approver's identity (e.g., verify the Slack user ID matches the expected approver).
This pattern ensures every approval request reaches a terminal state. No expense report sits in limbo forever, and the escalation chain provides accountability at every level.
**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.