Tips > Reliability & Performance

Use the Execution Resume URL in Wait Node Workflows for Approval Flows with Timeout Handling

The `$execution.resumeUrl` is a unique URL generated by the Wait node that, when called, resumes the paused execution.

The Wait node generates a unique $execution.resumeUrl that resumes a paused execution when it is called. Use it to build approval flows where a person clicks Approve or Reject in Slack, with a timeout on every Wait so nothing hangs forever. Chain the waits to escalate -- manager, then VP, then auto-reject -- so every request reaches a terminal state.

What is the execution resume URL in n8n?

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]

How do you build an approval message with the resume URL?

Building the approval message with the resume URL:

// 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
  }
}];

Wait node configuration with webhook resume:

{
  "resume": "webhook",
  "options": {
    "webhookSuffix": "/expense-approval",
    "responseMode": "lastNode",
    "timeout": 72,
    "timeoutUnit": "hours"
  }
}

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):

// 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 }];

How do you handle approve, reject, and timeout outcomes?

Handle all three outcomes (approved, rejected, timeout):

// 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'
  }
}];
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

How do you keep the resume URL secure?

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 · Break Large Workflows into Sub-Workflows

Showcase builds

19 complete workflows from my own projects, each with its n8n workflow JSON to import. Showcase entries link the file at the end of the article.

See the showcase builds

Keep reading

190 entries grouped by topic, from first workflow to queue mode. Free, no signup.

Browse the encyclopedia

Need it built?

I design, build and run n8n systems for clients. Every engagement starts with a $1,500 diagnostic audit, credited toward the build.

Book an introductory call