Skip to content

Step Delay

Ductwork Pro lets you tell a step to wait before it runs. When the pipeline advances to a delayed step, its job is created with a start time in the future, and job workers leave it alone until that time passes.

Nothing is held open while waiting. There’s no sleeping thread and no special “waiting” status. The delay is just a timestamp on the job’s availability record, so a pipeline with a week-long delay in the middle costs you one row in the database and nothing else.

Add a delay option to any transition in your pipeline definition. Values can be integers (seconds) or ActiveSupport::Duration objects:

define do |pipeline|
pipeline.start(SendWelcomeEmail)
.chain(to: CheckProfileComplete, delay: 1.day)
.chain(to: SendReminderEmail, delay: 3.days)
end

Here CheckProfileComplete runs a day after SendWelcomeEmail finishes, and SendReminderEmail runs three days after that. Each delay is measured from when the previous step completed, not from when the pipeline was triggered.

Anything other than an integer or duration raises an ArgumentError when the pipeline class is loaded, as does a negative value.

A delay on start delays the whole pipeline. The first job is enqueued at trigger time with a start time in the future:

define do |pipeline|
pipeline.start(SendFollowUp, delay: 2.hours)
end

You can also decide this at trigger time. trigger accepts either a delay (a duration or integer) or a delay_until (a timestamp). Passing one overrides whatever delay is in the definition. Passing both raises an ArgumentError, and so does a delay_until in the past.

SendFollowUpPipeline.trigger(user.id, delay: 30.minutes)
SendFollowUpPipeline.trigger(user.id, delay_until: user.trial_ends_at)

A delay is a lower bound. Once the start time passes, the job is picked up on the next job worker poll, so the real wait is the delay plus up to one polling interval. Start times are computed with the database clock, so it doesn’t matter whether your app servers and workers agree on what time it is.

A step can have both a delay and a timeout. The timeout clock doesn’t start until the step actually begins executing, so a long delay never eats into the timeout budget:

pipeline.chain(to: CallExternalApi, delay: 5.minutes, timeout: 30.seconds)

Delays work on every transition, not just chain. A delay on a fan-out like divide or expand applies to each step it creates. A delay on a fan-in like combine or collapse applies to the step that gathers the results, counting from when the last branch finished.