NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
EventWhenPayload
LifecycleTransitioningInside the transaction, before the write (a throwing listener aborts).subject, lifecycle, transition, from, to, actor, system
LifecycleTransitionedAfter commit, once per state change: kinds transition, expiry, scheduled, rollback, adopted.subject, subjectType, subjectId, lifecycle, kind, transition, from, to, actor, system, historyId, version
LifecycleTransitionDeniedAfter the refused attempt rolled back (even when your own transaction then rolls back).subjectType, subjectId, lifecycle, transition, actor, denials
LifecycleExpiredAfter commit, with the expiry’s LifecycleTransitioned.subject, lifecycle, from, to, historyId, expiresAt
LifecycleExpiringAfter commit, once per warning lead.subject, lifecycle, state, expiresAt, lead, scheduleId
LifecycleRolledBackAfter commit, once per rollback call.subject, lifecycle, from, to, revertedIds, rollbackIds, actor
LifecycleFrozen / LifecycleUnfrozenAfter commit.subject, lifecycle, until, reason, actor
LifecycleAdoptedAfter commit.subject, lifecycle, recordedState, actualState, historyId
ScheduledTransitionFailedAfter commit.scheduleId, subjectType, subjectId, lifecycle, transition, denials, attempts, final

Listen for LifecycleTransitioned rather than Eloquent’s updated: the state write is a direct compare-and-swap, so model events do not fire for it. It covers every state change — transitions, expiries, scheduled runs, rollback steps and adoptions — but not creation, where your model’s created event and the initial history row are the signal:

use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Lifecycle\Enums\TransitionKind;
use RoundlyConsulting\Lifecycle\Events\LifecycleTransitioned;

Event::listen(function (LifecycleTransitioned $event): void {
    if ($event->subject instanceof Listing && $event->to === ListingStatus::Active) {
        SearchIndex::add($event->subject);
    }

    if ($event->kind === TransitionKind::Rollback) {
        AuditLog::record("Rolled back to {$event->to->value}", $event->historyId);
    }
});
  • LifecycleTransitioning runs inside the transaction; a throwing listener aborts and rolls everything back, but denials belong in guards. A deadlock retry runs it again.
  • LifecycleTransitionDenied is deliberately not after-commit — it fires even when your own outer transaction then rolls back because of the denial.
  • After-commit events inside your own transaction are held until your outermost transaction commits, and discarded if it rolls back.
  • Payloads never contain the transition payload. Read the history row (historyId) when you need the stored, validated context.
  • Queued listeners should re-fetch the subject by subjectType / subjectId.
  • Idempotent replays and Lifecycles::fake() fire nothing.

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

By donating, you agree to our donation terms.

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.