Skip to content

Lifecycle Events ​

Filament Flow dispatches Laravel events at key points during workflow execution. Listening to these events lets you react to state changes without modifying the workflow engine itself — for example, to sync external systems, update caches, send custom notifications, or write audit records.

Events fire for both code-first (Spatie State class) and database-driven transitions. They are dispatched after the transition has been committed and side effects have run, so listeners receive the record in its final state.

StateEntered ​

Fired when a record enters a new state.

php
namespace RoBYCoNTe\FilamentFlow\Events;

class StateEntered
{
    public function __construct(
        public readonly Model $record,
        public readonly string $state,  // fully-qualified class name or state string
        public readonly ?Model $user = null,
    ) {}
}

Example listener:

php
namespace App\Listeners;

use RoBYCoNTe\FilamentFlow\Events\StateEntered;

class SyncExternalSystemOnStateEntered
{
    public function handle(StateEntered $event): void
    {
        // $event->record  — the model that entered the state
        // $event->state   — e.g. 'App\States\Order\ProcessingState' or 'processing'
        // $event->user    — the user who triggered the transition, or null

        if ($event->state === \App\States\Order\ProcessingState::class) {
            app(\App\Services\ExternalOrderSync::class)->markAsProcessing($event->record);
        }
    }
}

StateExited ​

Fired when a record exits a state, immediately before entering the next one.

php
namespace RoBYCoNTe\FilamentFlow\Events;

class StateExited
{
    public function __construct(
        public readonly Model $record,
        public readonly string $state,  // the state being exited
        public readonly ?Model $user = null,
    ) {}
}

Example listener:

php
namespace App\Listeners;

use RoBYCoNTe\FilamentFlow\Events\StateExited;

class CleanUpOnStateExited
{
    public function handle(StateExited $event): void
    {
        if ($event->state === \App\States\Order\PendingState::class) {
            // Clear any pending hold flags when leaving the pending state
            cache()->forget("order_hold_{$event->record->id}");
        }
    }
}

TransitionCompleted ​

Fired after a transition has been fully executed (state updated, side effects run, history logged).

php
namespace RoBYCoNTe\FilamentFlow\Events;

class TransitionCompleted
{
    public function __construct(
        public readonly Model $record,
        public readonly string $from,      // the previous state
        public readonly string $to,        // the new state
        public readonly ?Model $user = null,
        public readonly array $metadata = [],  // form data submitted with the transition
    ) {}
}

Example listener:

php
namespace App\Listeners;

use RoBYCoNTe\FilamentFlow\Events\TransitionCompleted;

class NotifySlackOnTransitionCompleted
{
    public function handle(TransitionCompleted $event): void
    {
        \Illuminate\Support\Facades\Log::info('Transition completed', [
            'record'   => get_class($event->record) . '#' . $event->record->getKey(),
            'from'     => $event->from,
            'to'       => $event->to,
            'user'     => $event->user?->email,
            'metadata' => $event->metadata,
        ]);
    }
}

WorkflowAssigned ​

Fired when a user is assigned to a record via the workflow assignment system.

php
namespace RoBYCoNTe\FilamentFlow\Events;

class WorkflowAssigned
{
    public function __construct(
        public readonly Model $record,
        public readonly Model $assignee,
        public readonly ?Model $assignedBy = null,
        public readonly string $assignmentType = 'primary',
    ) {}
}

Example listener:

php
namespace App\Listeners;

use RoBYCoNTe\FilamentFlow\Events\WorkflowAssigned;

class NotifyAssigneeOnWorkflowAssigned
{
    public function handle(WorkflowAssigned $event): void
    {
        // $event->record          — the model that was assigned
        // $event->assignee        — the user being assigned
        // $event->assignedBy      — the user who made the assignment, or null (system)
        // $event->assignmentType  — e.g. 'primary', 'secondary', 'reviewer'

        $event->assignee->notify(
            new \App\Notifications\YouHaveBeenAssigned($event->record, $event->assignedBy)
        );
    }
}

Registering Listeners ​

Register listeners in AppServiceProvider (Laravel 11+) or in EventServiceProvider:

AppServiceProvider (Laravel 11+):

php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use RoBYCoNTe\FilamentFlow\Events\StateEntered;
use RoBYCoNTe\FilamentFlow\Events\StateExited;
use RoBYCoNTe\FilamentFlow\Events\TransitionCompleted;
use RoBYCoNTe\FilamentFlow\Events\WorkflowAssigned;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        \Illuminate\Support\Facades\Event::listen(
            StateEntered::class,
            \App\Listeners\SyncExternalSystemOnStateEntered::class,
        );

        \Illuminate\Support\Facades\Event::listen(
            TransitionCompleted::class,
            \App\Listeners\NotifySlackOnTransitionCompleted::class,
        );

        \Illuminate\Support\Facades\Event::listen(
            WorkflowAssigned::class,
            \App\Listeners\NotifyAssigneeOnWorkflowAssigned::class,
        );
    }
}

EventServiceProvider (Laravel 10 and below):

php
protected $listen = [
    \RoBYCoNTe\FilamentFlow\Events\TransitionCompleted::class => [
        \App\Listeners\NotifySlackOnTransitionCompleted::class,
    ],
    \RoBYCoNTe\FilamentFlow\Events\WorkflowAssigned::class => [
        \App\Listeners\NotifyAssigneeOnWorkflowAssigned::class,
    ],
];

Queued Listeners ​

All four events use the SerializesModels trait, which means Eloquent model instances are serialized safely when listeners are queued. Laravel will automatically re-fetch the models from the database when a queued listener is processed.

php
namespace App\Listeners;

use Illuminate\Contracts\Queue\ShouldQueue;
use RoBYCoNTe\FilamentFlow\Events\TransitionCompleted;

class SyncExternalOrderOnTransitionCompleted implements ShouldQueue
{
    public string $queue = 'workflows';

    public function handle(TransitionCompleted $event): void
    {
        // $event->record is automatically restored from the database
        app(\App\Services\ExternalOrderSync::class)->sync($event->record);
    }
}

Event Dispatch Order ​

For a standard state-changing transition the events fire in this order:

  1. StateExited — the record is leaving its previous state
  2. StateEntered — the record has entered the new state
  3. TransitionCompleted — the entire transition is complete

For in-state actions (transitions with no target state) only TransitionCompleted is dispatched, with from and to set to the same state value.

Proprietary software. All rights reserved.