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:
- Run
SendWelcomeEmailHandler - Wait 1 day
- Run
SendFollowUpEmailHandler - Wait 3 days
- Run
SendFinalEmailHandler
Duration parameters
The sleep() method accepts any combination of named duration parameters:
| Parameter | Type | Description |
|---|---|---|
$seconds | int | Seconds to wait |
$minutes | int | Minutes to wait |
$hours | int | Hours to wait |
$days | int | Days 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 totalHow 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_attimestamp.
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 );