Skip to main content

Expiration & monitoring

Runs, actions, and signal waits can carry deadlines — set explicitly (->expiresAt(...), ->timeoutAfter(...), #[FlowTimeout]) or via the configured defaults in monitor.expiration.defaults. Something has to notice an expired deadline; there are two ways to drive the sweep.

Default deadlines

'monitor' => [
'expiration' => [
'defaults' => ['run' => 3600, 'action' => 600, 'signal' => 86400],
],
],

Applied at write time when no explicit deadline is set: run on create, action on schedule, signal on await. null = off. There is no per-entity opt-out flag; to bypass a default for one entity, pass an explicit (far-future) deadline.

Driving the sweep

use Illuminate\Support\Facades\Schedule;

Schedule::command('saga-flow:monitor')->everyMinute();

Queue looping (opt-in)

Drive the sweep off the queue worker's idle loop instead of cron:

'monitor' => [
'queue_looping' => ['enabled' => true, 'throttle_seconds' => 30],
],

Useful when you have always-on workers but no scheduler. The sweep is throttled so it runs at most once per throttle_seconds.

Repair (the doctor)

Separate from expiration: the doctor recovers a run whose progress was lost to a dropped job — an action that never ran, a resume that never fired — rather than one that hit a deadline. It only ever re-dispatches existing jobs or re-wakes flows (replay decides the rest); it never creates duplicate work.

'repair' => [
'enabled' => true,
'grace_seconds' => 60,
'max_attempts' => 10,
'redispatch_actions' => true,
'wake_waiting' => true,
],

Schedule it, or loop it off the worker:

Schedule::command('saga-flow:repair')->everyFiveMinutes();

To re-drive a single stuck run by hand:

SagaFlow::kick($runId); // or:
// php artisan saga-flow:kick {run}

Pruning

Delete old terminal runs and their related rows:

php artisan saga-flow:prune --days=90
php artisan saga-flow:prune --before=2026-01-01 --dry-run

The default retention window is prune.retention_days.