---
title: "Scheduled transitions and the sweep — Lifecycle for Laravel | Roundly"
description: "Schedule any transition for later and run due expiries, warnings and schedules with lifecycle:sweep — inline or queued, with retries."
url: https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel/scheduled-transitions
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Lifecycle for Laravel](https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel.md)

# Scheduled transitions and the sweep

Schedule a transition for an instant; the sweep runs it as the system:

```php
// The transition must be allowSystem() (or systemOnly() and scheduled with asSystem()).
$scheduled = Lifecycles::for($listing)->by($editor)->because('Launch')->schedule('publish', $publishAt);
Lifecycles::for($listing)->asSystem()->schedule('expire', $at);   // a systemOnly() transition

Lifecycles::for($listing)->scheduled();                 // list<ScheduledTransition>
Lifecycles::for($listing)->cancelScheduled('publish');  // bool
Lifecycles::for($listing)->retryScheduled('publish');   // bool: this listing's newest failed one, back to pending
```

Scheduling checks the structure, freeze and seal, the actor rules, reason and payload now, against the stored state; time rules, quotas and rate limits are checked when it runs. A refusal throws TransitionDeniedException. A user scheduling a systemOnly() transition is refused with system\_only, and a transition that marks a sensitive() key required cannot be scheduled (invalid\_payload), because schedules never store sensitive keys. One pending schedule exists per transition (and one expiry), and scheduling again replaces it. Leaving the state it was created in cancels it.

## The sweep

php artisan lifecycle:sweep (every minute) sends due warnings, then runs due schedules in batches, each in its own transaction, in system context:

- Refused for temporary reasons: retried after schedules.retry\_after (or when the denial says), up to schedules.max\_attempts, then failed. A permanent denial fails at once. Both fire ScheduledTransitionFailed.
- Frozen subject: deferred, without using up an attempt — unless the transition ignoresFreeze(), which runs while frozen, as a direct apply() does.
- Soft-deleted subject: its schedules are paused until it is restored (a restore without model events, such as restoreQuietly(), is picked up by the next sweep). A deleted subject cancels its schedules.
- An exception: it is reported to your exception handler and counted as an attempt. The rest of the sweep continues. A warning that throws is reported too and never stops the sweep; a warning whose model, lifecycle or state no longer exists stops warning.

```php
Lifecycles::sweep();                                  // SweepResult: warned, executed, deferred, failed, errored, cancelled, skipped, queued
Lifecycles::sweep(limit: 100, queue: true);           // dispatch RunScheduledTransitionJob per schedule
Lifecycles::schedules()->runDue();                    // due schedules only
Lifecycles::schedules()->warn();                      // warnings only
Lifecycles::schedules()->due();                       // Collection<ScheduledTransition>, read-only
Lifecycles::schedules()->failed();                    // Collection<ScheduledTransition>, most recently failed first
Lifecycles::schedules()->retry($scheduleId);          // failed → pending, attempts reset
Lifecycles::sweep(connection: 'tenant');              // the package tables of another connection
```

SweepResult counts warned, executed, deferred, failed, errored, cancelled, skipped and queued, plus total(). “Now” is always the clock — there is no way to sweep “as of” a future instant; in tests, travel with Carbon::setTestNow().

## Database connections

Package rows live on their subject’s connection, and one sweep covers one connection’s package tables: the default, or the one named with lifecycle:sweep --database=<connection>, Lifecycles::sweep(connection: …) or SweepOptions::$connection. Schedule one sweep per connection your subjects use — lifecycle:validate warns about a listed model whose connection has none. Schedule ids are per connection: pass it to schedules()->retry($id, $connection), while a handle’s retryScheduled() uses its subject’s connection.

## Failed schedules

A ScheduledTransition says which subject it belongs to (subjectType, subjectId) and, once it has finished, how it ended (status, outcome, lastDenial, finishedAt). schedules()->failed() lists the failed ones; retry one by id with schedules()->retry(), or per subject with retryScheduled() — an expiry is retried by the name of its expiry transition (retryScheduled('expire')). To be alerted when a schedule gives up, listen for ScheduledTransitionFailed:

```php
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use RoundlyConsulting\Lifecycle\Events\ScheduledTransitionFailed;

Event::listen(function (ScheduledTransitionFailed $event): void {
    Log::warning('Scheduled transition failed', ['schedule' => $event->scheduleId, 'transition' => $event->transition]);
});
```

## Queued sweeps

With --queue, queue: true or schedules.queue.enabled, the sweep dispatches one RunScheduledTransitionJob per due schedule on schedules.queue.connection / schedules.queue.name. Each schedule gets one unique job, even when the queue stalls between sweeps, and the job re-checks everything under the lock — the retry and failure accounting is the same as inline.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel.md)
