When to Use
- Rate limiting between API calls: Insert a 1-second delay inside a Loop body to space out external API calls and avoid hitting per-second rate limits — for example, sending 500 SMS messages one per second rather than in a burst.
- Scheduled follow-up communication: After sending a welcome email, pause for 3 days then send an onboarding follow-up. Use
delay_type: "duration" with duration_ms: 259200000 (72 hours) or drive the value from a workflow variable.
- SLA enforcement and escalation: After a support ticket is created, delay 2 hours. If not resolved when the workflow resumes, trigger an escalation notification to a supervisor automatically.
- Timed content or feature release: Hold a workflow at the Delay node until a specific UTC datetime — the product launch window — then proceed to publish content, flip a feature flag, or send a coordinated announcement.
- Retry back-off: After a failed payment attempt, delay 5 minutes before retrying the charge. Use an expression to compute exponential back-off durations across multiple retry iterations.
Configuration
| Field | Required | Default | Description |
delay_type |
Required |
— |
Selects the delay strategy. Must be "duration" (pause for a calculated number of milliseconds) or "until" (pause until a specific UTC datetime is reached). |
duration_ms |
Optional |
— |
duration mode only. Exact milliseconds to delay. Checked first in the resolution chain. Must be a non-negative integer. Example: 5000 for a 5-second delay. |
duration_seconds |
Optional |
— |
duration mode only. Seconds to delay — converted to milliseconds internally. Checked after duration_ms. Useful for human-readable config values. |
duration_minutes |
Optional |
— |
duration mode only. Minutes to delay — converted to milliseconds internally. Checked after duration_seconds. |
duration_expression |
Optional |
— |
duration mode only. A BizFirst expression that evaluates to a number of milliseconds at runtime. Used as fallback when none of the literal duration fields are set. Example: {{ $var.retry_delay_ms }}. |
until_utc |
Optional |
— |
until mode only. Target UTC datetime as an ISO-8601 string (e.g. "2026-06-01T09:00:00Z"). Checked first in the until resolution chain. |
until_expression |
Optional |
— |
until mode only. A BizFirst expression evaluating to a UTC datetime string. Fallback when until_utc is not set. Example: {{ $json.scheduledLaunchAt }}. |
in_process_threshold_ms |
Optional |
30000 |
Delays shorter than this threshold use Task.Delay (in-process, blocks a thread). Delays at or above this threshold create a PendingDelay record in the database and free the thread (durable suspension). Default is 30 seconds (30,000 ms). |
Duration resolution order: For delay_type: "duration", the node resolves the delay amount in this order: duration_ms → duration_seconds → duration_minutes → duration_expression. The first non-null value is used. If all four are absent, a validation error is raised.
Sample Configuration
Fixed 5-minute duration delay
{
"delay_type": "duration",
"duration_minutes": 5
}
Dynamic duration from a workflow variable (retry back-off)
{
"delay_type": "duration",
"duration_expression": "{{ $var.retry_delay_ms }}"
}
Until a specific UTC datetime (launch window)
{
"delay_type": "until",
"until_utc": "2026-09-01T08:00:00Z"
}
Until a datetime from trigger data (contract start date)
{
"delay_type": "until",
"until_expression": "{{ $json.contractStartDate }}"
}
Short in-process delay with custom threshold
{
"delay_type": "duration",
"duration_ms": 1000,
"in_process_threshold_ms": 5000
}
Validation Errors
| Error Message | Cause |
delay_type must be 'duration' or 'until' | The delay_type field is missing, empty, or contains a value other than "duration" or "until". |
duration mode requires duration_ms, duration_seconds, duration_minutes, or duration_expression | All four duration source fields are absent when delay_type is "duration". |
until mode requires until_utc or until_expression | Both until_utc and until_expression are absent when delay_type is "until". |
Delay duration must be non-negative, got {ms}ms | A resolved duration value is negative. Check computed expressions that subtract timestamps — negative values occur when the source datetime is in the future relative to the current time. |
duration_expression '{value}' could not be parsed as a number | The expression resolved to a non-numeric string or object. Ensure the expression evaluates to an integer or float representing milliseconds. |
until_utc '{value}' is not a valid ISO-8601 UTC datetime | The literal or expression-resolved value for until_utc cannot be parsed as a valid UTC datetime. Check timezone suffix — bare datetimes without Z or offset are rejected. |
Output / Execution Ports
| Port | Fires When |
success |
The delay elapsed normally: the in-process timer completed, the durable delay was resumed by the scheduler, or a past until_utc was skipped (mode: "skipped"). |
waiting |
A durable delay was initiated — fires immediately after the PendingDelay record is persisted to the database. The workflow thread is freed. Use this port to log or monitor pending delays. |
cancelled |
An in-process delay was cancelled externally before it elapsed (e.g. a workflow cancellation signal was received). Not applicable to durable delays — durable suspensions are resumed or expired, not cancelled mid-wait. |
error |
Configuration error detected at runtime: negative duration, unparseable datetime, missing required fields, or expression evaluation failure. |
success and cancelled Ports — Output Fields
| Field | Type | Description |
delay.requested_ms | long | The delay duration that was requested, in milliseconds. For until mode, this is the computed difference between the target time and the actual start time. |
delay.actual_ms | long | The actual elapsed milliseconds from when the delay started to when it completed. May differ slightly from requested_ms due to scheduler resolution. 0 if mode is "skipped". |
delay.resumed_at | string (ISO-8601) | UTC timestamp of when the delay completed and the workflow resumed execution. |
delay.mode | string | "in_process" — used Task.Delay; "durable" — used PendingDelay (thread was freed); "skipped" — until_utc was in the past. |
waiting Port — Output Fields
| Field | Type | Description |
delay.id | int | The PendingDelay database record ID. Log this to enable manual resumption or monitoring. |
delay.requested_ms | long | Requested delay duration in milliseconds. |
delay.resume_after | string (ISO-8601) | Scheduled UTC datetime when the durable delay will resume the workflow. |
delay.mode | string | Always "durable" on this port. |
Sample Output
success port (durable delay completed)
{
"delay": {
"requested_ms": 259200000,
"actual_ms": 259200341,
"resumed_at": "2026-05-29T09:00:00Z",
"mode": "durable"
}
}
waiting port (durable delay initiated)
{
"delay": {
"id": 8821,
"requested_ms": 259200000,
"resume_after": "2026-05-29T09:00:00Z",
"mode": "durable"
}
}
success port (until mode, target was in the past — skipped)
{
"delay": {
"requested_ms": 0,
"actual_ms": 0,
"resumed_at": "2026-05-26T14:22:11Z",
"mode": "skipped"
}
}
Expression Reference
| Expression | Result |
{{ $output.waitDelay.delay.resumed_at }} | UTC timestamp when the delay completed — useful for audit logging. |
{{ $output.waitDelay.delay.mode }} | Which strategy was used: "in_process", "durable", or "skipped". |
{{ $output.waitDelay.delay.actual_ms }} | Actual elapsed wait in milliseconds — use for SLA measurement. |
{{ $output.waitDelay.delay.id }} | PendingDelay record ID (available on waiting port) — store for monitoring or manual resume. |
{{ $output.waitDelay.delay.resume_after }} | Scheduled resume UTC time (available on waiting port) — useful for displaying in a dashboard. |
Do not use Delay as a substitute for EventWait: If you are waiting for an external event (a webhook, a user action, a message arriving on a queue), use the
EventWait node. Delay only waits for a fixed time — it has no mechanism to detect or correlate external events. Using Delay with a timeout as a polling substitute is an anti-pattern that wastes compute and misses events that arrive early.
Node Policies & GuardRails
| Policy Area | Recommendation |
| Durable delays for long waits |
Any delay longer than 1 minute should use durable suspension (ensure in_process_threshold_ms is at or below 60,000). In-process delays that span deployments are lost when the server restarts — durable delays persist to the database and survive restarts. |
| EventWait vs Delay |
Use Delay only for time-based pauses. For waits that should be cut short by an external event, use EventWait with a timeout_seconds fallback. Combining both is the correct pattern for "wait up to X minutes for an event, else continue". |
| waiting port monitoring |
Connect the waiting port to a logging or notification node in production workflows. The delay.id on this port is the only way to identify and manually resume a durable delay if the scheduler encounters an issue. |
| UTC-only datetime values |
All values for until_utc and until_expression must be ISO-8601 UTC strings ending in Z or a UTC offset. Localised datetime strings without timezone information are rejected to prevent ambiguous scheduling across server timezones. |
| Configurable delays via variables |
Set duration_expression to a workflow variable (e.g. {{ $var.follow_up_delay_ms }}) for delays that need to be tuned without redeploying the workflow. Store the value in a configuration workflow or environment setting. |
| Negative duration safety |
When computing a duration by subtracting two timestamps, always guard against a negative result before passing to the Delay node. A negative computed value will route to the error port. Use an IfCondition or a Function node to clamp the value to 0 or a minimum positive value if needed. |
Examples
Example 1 — 3-Day Follow-Up Email After Onboarding
After a new user registers and receives a welcome email, the workflow pauses for 3 days using a durable delay, then sends a follow-up email checking on their progress. The durable delay survives any server restarts during the 3-day window.
WebhookTrigger [key: "userRegistered"]
(POST /users/register)
└─► EmailSmtp [key: "sendWelcome"]
To: {{ $json.email }}
Subject: "Welcome to BizFirstAI"
└─► Delay [key: "waitThreeDays"]
delay_type: "duration"
duration_ms: 259200000 // 3 days in ms
├─► waiting ──► Slack (log: "Follow-up pending for {{ $json.email }}, delay.id: {{ $output.waitThreeDays.delay.id }}")
└─► success ──► EmailSmtp [key: "sendFollowUp"]
To: {{ $json.email }}
Subject: "How are you getting on?"
Example 2 — Rate-Limited API Calls in a Loop
A batch job processes 500 customer records and must call an external enrichment API for each. The API enforces a 1 request-per-second limit. A 1-second in-process delay between calls ensures compliance.
MongoDB [key: "getCustomers"]
(find all customers needing enrichment)
└─► Loop [key: "enrichLoop"]
Items: {{ $output.getCustomers.documents }}
├─► body ──► HttpRequest [key: "enrichCustomer"]
│ GET /enrich/{{ $var.current_item.customerId }}
│ └─► Delay [key: "rateLimitPause"]
│ delay_type: "duration"
│ duration_ms: 1000 // 1 second — in-process (below 30s threshold)
│ └─► success ──► MongoDB (update customer with enriched fields)
└─► done ──► Slack (batch complete: {{ $output.enrichLoop.collection_size }} customers enriched)
Example 3 — SLA Escalation After 2-Hour Wait
After a support ticket is created, the workflow waits 2 hours using a durable delay. When it resumes, it checks the ticket status. If still open, an escalation notification is sent to the supervisor. The delay ID is logged immediately so the ops team can monitor pending escalations.
WebhookTrigger [key: "ticketCreated"]
(POST /support/tickets)
└─► Delay [key: "slaWindow"]
delay_type: "duration"
duration_minutes: 120 // 2-hour SLA window
├─► waiting ──► HttpRequest (POST /ops/pending-slas body: { delayId: {{ $output.slaWindow.delay.id }}, ticketId: {{ $json.ticketId }} })
└─► success ──► HttpRequest [key: "checkTicketStatus"]
GET /support/tickets/{{ $json.ticketId }}
└─► IfCondition
Condition: {{ $output.checkTicketStatus.status == "open" }}
├─► true ──► EmailSmtp (To: supervisor — escalation: ticket {{ $json.ticketId }} unresolved after 2h)
└─► false ──► StopWorkflow (status: success, message: "Ticket resolved within SLA")