FlowTruxFlowTrux/Docs
Docsworkflows

Long Delays & Callbacks

Pause workflows for hours or days, or until an external system responds.

Some workflows need to wait. A drip campaign sends three follow-up emails over a week. An approval flow blocks until a human clicks Approve in Slack. A scheduled retry kicks back in four hours after a flaky external API failed.

FlowTrux has two primitives for these patterns, and both share the same suspend/resume infrastructure: no resources are held during the wait, the run survives platform restarts, and resumption happens automatically at the right moment.

TL;DR

PatternWhen to useAction types
Long Delay"Wait N seconds/minutes/hours/days, then continue" - clock-basedOne Action node, type delay
Callback"Wait until this URL is hit, then continue" - event-basedTwo Action nodes: generate_callback → notify → wait_for_callback

Both pause the run with status PAUSED. Both resume from exactly where they left off, with the full state context intact ({{steps.*}}, {{global.*}}, {{trigger.*}}, loop context).


Long Delay

A delay Action just waits. Configure duration (number) and unit (ms, s, m, h). Maximum 7 days.

There are two execution modes - picked automatically by the engine based on the requested duration:

DurationModeWhat happens
≤ 10 secondsInline waitThe run simply pauses in place, used for very short waits.
> 10 seconds, up to 7 daysSuspend → resumeThe run is fully suspended: its state is saved and a timer wakes it up.

You don't pick the mode, the engine picks it for you. Behavior is identical from the outside: the next node runs after the requested time, regardless of mode.

Output

Both modes produce a delay output you can reference downstream:

{
  "delayed": true,
  "durationMs": 3600000,
  "requested": { "duration": 1, "unit": "h" }
}

A long delay (suspend mode) reports slightly different fields:

{
  "delayed": true,
  "longDelay": true,
  "delayMs": 3600000,
  "resumeAt": "2026-04-30T15:00:00.000Z",
  "requested": { "duration": 1, "unit": "h" }
}

When to use it

  • Drip campaigns - send → wait 24h → send → wait 3d → send.
  • Cool-downs after a failure - try → on error, wait 30 minutes → retry once.
  • Scheduled follow-ups - created a ticket → wait 7 days → check whether it's still open and ping the owner.

When NOT to use it

  • You're waiting for an external event (a user click, an inbound webhook, a 3rd-party callback). Use the Callback pattern below instead - Long Delay can't be triggered early.
  • You want a recurring schedule (every Monday at 9am). Use a cron Trigger on a separate workflow.

Callbacks (Wait for external input)

The Callback pattern pauses a workflow until an external system POSTs to a URL the workflow itself generated. Two cooperating Action types:

generate_callback

Generates a unique callback URL and a 192-bit random token, and stores the token on the Execution record. Place this before the notification that sends the URL out.

Config:

FieldDefaultNotes
timeout3600 (1 hour)Seconds the workflow will wait. Maximum 86,400 (24h).

Output:

{
  "callbackUrl": "https://your-domain.com/api/webhooks/callback/<token>",
  "token": "<192-bit random token>",
  "timeout": 3600,
  "expiresAt": "2026-04-30T16:00:00.000Z"
}

wait_for_callback

Pauses the workflow until the URL is hit. Status flips to PAUSED, and a timeout is scheduled using expiresAt from the matching generate_callback.

Config:

FieldNotes
tokenReference the upstream node's token: {{steps.<generate-callback-id>.output.token}}

Output (after resume):

{
  "resumed": true,
  "callbackData": { "...": "the JSON or form body the caller POSTed" }
}

The flow

Trigger
  └─> generate_callback   ← creates URL + token
       └─> Slack/Gmail    ← sends URL to approver, embedded in a button or link
            └─> wait_for_callback   ← workflow PAUSES here
                 └─> branch on callbackData.action == "approve" / "reject"
                      └─> notify result, finish
  1. generate_callback produces the URL + token, attached to the current execution.
  2. The notification node embeds callbackUrl into a Slack message, an email, an SMS, or anywhere else the approver will see it.
  3. wait_for_callback puts the run into PAUSED.
  4. When the caller POSTs to the URL (an approval button wired to submit a POST, or an external system calling it programmatically), the workflow picks back up from its saved state.
  5. If the timeout fires before the URL is hit, the run fails with a timeout error.

The endpoint

POST /api/webhooks/callback/<token>
  • POST only. A plain link click in a browser issues a GET and will not resume the run - wire the approval so the click results in a POST (an HTML form button, an intermediary page, or the external system calling the URL directly).
  • Accepts JSON or form-encoded body.
  • The body becomes callbackData in the wait node's output; URL query parameters land in callbackData._query.
  • No HMAC required. The token itself is the auth - it's 192 bits of random and only valid for one specific paused execution.
  • The endpoint dedupes: a second POST with the same token after the first one resumes is a no-op.

Slack-approval example

Trigger (manual)
  → generate_callback (timeout: 86400)
  → slack.send_blocks (channel: ops, blocks: [
        {type: "section", text: "Deploy v1.42 to production?"},
        {type: "actions", elements: [
          {type: "button", text: "Approve",
           url: "{{steps.cb.output.callbackUrl}}?action=approve"},
          {type: "button", text: "Reject",
           url: "{{steps.cb.output.callbackUrl}}?action=reject"}
        ]}
     ])
  → wait_for_callback (token: {{steps.cb.output.token}})
  → if-else (condition: {{steps.wait.output.callbackData._query.action}} === "approve")
        true  → http.post (https://ci/deploy)
        false → slack.send_message (channel: ops, text: "Deploy cancelled by {{...}}")

The query-string trick (?action=approve) lets a single callback URL carry the user's choice - no separate URLs per button. The workflow reads callbackData._query.action after resume.

Note: the callback endpoint expects a POST. Slack url: buttons open the link in a browser (GET), so in practice point the buttons at a small approval page/handler that POSTs the callback URL with the chosen action - or have the approving system call the URL directly.

Behavior under restarts

  • PAUSED runs survive restarts. The run's state is saved at the moment of suspension, and the timeout timer survives with it.
  • After a restart, a callback POST still works - the run resumes normally.
  • The same is true for the timeout: if the run is still PAUSED when the timer fires, it is failed with a timeout error.

When to use it

  • Approval flows - Slack/email/Teams "Approve / Reject" buttons that drive a workflow.
  • Manual review checkpoints - pause a pipeline until QA hits a "ship it" link.
  • Two-system handoffs - your workflow kicks off work in an external system and wants to wait for that system's "done" callback before continuing.
  • OTP / 2FA-style waits - generate a magic link, send it, wait for the click.

When NOT to use it

  • You want to wait exactly N hours, not "until something happens". Use Long Delay.
  • The external system polls for status rather than pushing. Loop with a Long Delay between probes; when the status flips, exit the loop.
  • The wait is longer than 24 hours. The wait_for_callback timeout maxes at 86,400 seconds. For multi-day approval windows, generate a fresh callback inside a Long Delay loop, or split the workflow into two runs joined by a global variable / state record.

Comparing the two

Long DelayCallback
Trigger to resumeTime elapsedExternal POST
Can resume early?NoYes (any time before timeout)
Max wait7 days24 hours
Carries data on resume?NoYes - callbackData
Number of nodes1 (delay)2 (generate_callback + wait_for_callback)

Both share: PAUSED status, restart-safe, full state context preserved across resume, fully integrated with execution history.

  • Node Types - delay, generate_callback, wait_for_callback Action types
  • Webhooks - the auth model used for triggering workflows from outside (callbacks use a token-only variant)
  • Workflow Templates - several built-in templates use the callback pattern