Queuety
Workflows

Durable Timers

Workflows can pause for a fixed duration between steps using the sleep() builder method. Timer steps are durable: the delay is stored in the database, so it survives worker restarts, deploys, and server reboots.

The ->sleep() method

Add a timer step anywhere in the workflow chain:

use Queuety\Queuety;

Queuety::workflow( 'drip_campaign' )
    ->then( SendWelcomeEmailHandler::class )
    ->sleep( days: 1 )
    ->then( SendFollowUpEmailHandler::class )
    ->sleep( days: 3 )
    ->then( SendFinalEmailHandler::class )
    ->dispatch( [ 'user_id' => 42 ] );

The workflow will:

  1. Run SendWelcomeEmailHandler
  2. Wait 1 day
  3. Run SendFollowUpEmailHandler
  4. Wait 3 days
  5. Run SendFinalEmailHandler

Duration parameters

The sleep() method accepts any combination of named duration parameters:

ParameterTypeDescription
$secondsintSeconds to wait
$minutesintMinutes to wait
$hoursintHours to wait
$daysintDays to wait

All values are summed together. These are equivalent:

->sleep( seconds: 3600 )
->sleep( minutes: 60 )
->sleep( hours: 1 )

You can combine them:

->sleep( hours: 1, minutes: 30 ) // 90 minutes total

How timer steps work

When a workflow reaches a timer step, Queuety dispatches an internal __queuety_timer job with its available_at column set to the current time plus the delay duration. The job sits in the database and is not claimed by any worker until the delay has elapsed.

This means:

  • No polling. Workers do not repeatedly check the timer. The job simply becomes available when the time comes.
  • No memory usage. Nothing is running during the wait. The timer exists only as a database row.
  • Survives restarts. If the worker restarts, the timer job is still in the queue with the correct available_at timestamp.

When the timer job is claimed after the delay, the workflow advances to the next step automatically.

Example: delayed notifications

Send a reminder 1 hour after a user signs up, but only if they have not completed onboarding:

Queuety::workflow( 'onboarding_reminder' )
    ->then( CreateAccountHandler::class )
    ->sleep( hours: 1 )
    ->then( CheckOnboardingHandler::class )
    ->then( SendReminderHandler::class )
    ->dispatch( [ 'email' => 'user@example.com' ] );

The CheckOnboardingHandler step can inspect the state and use conditional branching to skip the reminder if onboarding is already complete.

Example: retry with backoff

Insert deliberate delays between retry-like steps:

Queuety::workflow( 'poll_external_api' )
    ->then( CheckStatusHandler::class, 'check' )
    ->sleep( minutes: 5 )
    ->then( CheckStatusHandler::class, 'recheck' )
    ->sleep( minutes: 15 )
    ->then( CheckStatusHandler::class, 'final_check' )
    ->then( ProcessResultHandler::class )
    ->dispatch( [ 'external_id' => 'abc-123' ] );

Multiple timers

A workflow can contain any number of timer steps. Each timer is independent and tracked by a sequential name (timer_0, timer_1, etc.) in the workflow state.

Queuety::workflow( 'multi_step_drip' )
    ->then( StepA::class )
    ->sleep( hours: 2 )
    ->then( StepB::class )
    ->sleep( days: 1 )
    ->then( StepC::class )
    ->sleep( minutes: 30 )
    ->then( StepD::class )
    ->dispatch( $payload );

On this page