Skip to content
Sleep and Timers

Sleep and Timers

A sleep step introduces a durable delay into a workflow – the engine handles the timer entirely, with no worker involved.

Overview

Sleep steps pause workflow execution for a specified duration. Unlike a time.Sleep() in application code, a DagNats sleep is durable: it survives engine restarts, NATS reconnections, and server reboots. The delay is stored as a WakeAt timestamp in the step state, and the engine uses the SLEEP_TIMERS JetStream stream with NakWithDelay to schedule the wake-up.

No worker participates in a sleep step. The engine publishes a timer message to SLEEP_TIMERS, the NATS consumer NAKs it with the requested delay, and when NATS redelivers the message after the delay expires, the engine publishes a step.sleep.completed event to advance the workflow.

This is distinct from the worker-level Pause() method, which checkpoints mid-task state and uses NakWithDelay to resume the same worker handler. Sleep steps are DAG-level delays between steps; Pause() is a within-step delay that keeps the step in Running status.

How It Works

    sequenceDiagram
    participant Engine
    participant SLEEP as SLEEP_TIMERS
    participant History as WORKFLOW_HISTORY

    Engine->>Engine: step ready, type = sleep
    Engine->>SLEEP: publish timer (action: sleep_complete)
    Engine->>History: step.sleep.started
    Note over SLEEP: NakWithDelay(duration)
    SLEEP-->>Engine: redeliver after delay
    Engine->>History: step.sleep.completed
    Note over Engine: downstream steps now ready
  

The SLEEP_TIMERS stream is shared across multiple timer use cases via an action discriminator in the message payload. Sleep steps use the sleep_complete action. The same stream handles wait-for-event timeouts (wait_timeout), rate-limit retries (rate_retry), and other scheduled operations.

The step’s WakeAt field in StepState records the expected completion time. This is purely informational – the actual wake-up is driven by NATS redelivery, not by polling the timestamp.

Usage

wf := dag.NewWorkflow("rate-limited-pipeline")

call := wf.Task("call-api", "api-call").
    WithTimeout(10 * time.Second)

cooldown := wf.Sleep("cooldown", 30*time.Second).
    After(call)

next := wf.Task("next-call", "api-call").
    After(cooldown)

def, err := wf.Build()

In workflow JSON the same step reads:

{
  "id": "cooldown",
  "type": "sleep",
  "depends_on": ["call-api"],
  "config": { "duration": "30s" }
}

Configuration

Sleep configuration is stored in StepDef.Config as SleepConfig:

FieldTypePurpose
durationdurationHow long to sleep. Must be positive. Fixed when the def is registered. In JSON it is a string such as "30s" or integer nanoseconds.
cronstringSleep until the next occurrence of this 5-field cron expression, strictly after dispatch.
until_input_pathstringDot-path into the run input naming the deadline. Resolved at dispatch.
cron_timezonestringIANA zone the cron form is evaluated in, e.g. America/Chicago. Validated at registration.
cron_timezone_input_pathstringDot-path into the run input naming the zone for the cron form. Resolved at dispatch.

Exactly one of duration, cron and until_input_path must be set; a config with zero or more than one is rejected when the workflow def is registered, as is a malformed cron expression.

duration fixes the delay at registration time. cron and until_input_path defer it to dispatch, which is what makes calendar waits and payload-carried deadlines expressible as sleep steps rather than as a worker that blocks on a task lease.

cron form:

Uses the same 5-field grammar as cron triggers (*, */N, N-M, comma lists; numeric day-of-week, no symbolic names). When both day-of-month and day-of-week are restricted, a day matches if either one matches (the POSIX rule), so 0 9 1 * 1 is the 1st or any Monday.

// Each value is what you put in StepDef.Config of a sleep step.

// Next Monday at 09:00 UTC.
utc := dag.MarshalConfig(&dag.SleepConfig{Cron: "0 9 * * 1"})

// Next Monday at 09:00 in Chicago.
chicago := dag.MarshalConfig(&dag.SleepConfig{
	Cron: "0 9 * * 1", CronTimezone: "America/Chicago",
})

// Next Monday at 09:00 in the zone the run input names, e.g.
// {"subscriber": {"timezone": "Asia/Tokyo"}}.
fromInput := dag.MarshalConfig(&dag.SleepConfig{
	Cron: "0 9 * * 1", CronTimezoneInputPath: "subscriber.timezone",
})

A cron sleep is evaluated in UTC unless cron_timezone or cron_timezone_input_path names another zone; at most one of the two may be set, and only with cron. The host’s local zone is never used, so a def fires at the same instant on every machine. An unknown static zone fails registration. A zone read from the run input can only be checked at dispatch: when the path is missing or names no known zone, the sleep falls back to UTC and the engine logs a warning, rather than failing the run.

Daylight-saving transitions need no special handling: on spring-forward day a time that does not exist (02:30 in Chicago) does not match, and on fall-back day a repeated time matches at its first occurrence.

until_input_path form:

The path resolves against the run input – not the step’s resolved input, which for a mid-DAG step is upstream output and the wrong source for a run-scoped deadline. The target value is either an RFC3339 instant or a number of milliseconds.

// Run input: {"deadline": "2026-08-01T12:00:00Z"}
cfg := dag.MarshalConfig(&dag.SleepConfig{UntilInputPath: "deadline"})

An RFC3339 instant already in the past clamps to a zero-length sleep that completes normally. A missing path, an unparseable value, or a cron expression with no next occurrence fails the step at dispatch.

Bounds:

  • Maximum: 365 days
  • The bound applies to the resolved delay of all three forms, so a cron or until_input_path wait longer than 365 days fails the step at dispatch
  • The engine sets StepState.WakeAt to now + duration when the sleep starts

Sleep vs Pause:

Sleep StepWorker Pause
ScopeDAG-level, between stepsWithin a step handler
WorkerNoneSame worker resumes
Step statusTransitions through started/completedStays Running
Builderwf.Sleep(id, duration)ctx.Pause(name, duration)
MechanismSLEEP_TIMERS streamCheckpoint + NakWithDelay

Related