What Is Saga Lara Flow?
Saga Lara Flow is a Laravel package by Andriy Karpishyn that lets you model long-running business processes — charge a card, reserve stock, book a shipment — as a single handle() method on top of Laravel queues. No job chaining, no hand-rolled state machines.
The engine records every completed step to the database. When a worker picks up a workflow, it re-executes handle() from the top, but $this->action() intercepts each call: already-completed steps return their stored result instantly, and execution resumes only at the first unfinished step. If any step throws, registered compensations fire in reverse order.
Core Features at a Glance
- Workflows as plain methods — sequential
$this->action()calls with no job chaining - Compensating transactions — register an undo action per step with
compensateWith() - Signals — suspend a run until external input arrives, with optional timeout
- Parallel blocks — dispatch independent actions concurrently and collect results
- Child workflows — nest workflows with a configurable close policy
- Side effect recording — wrap non-deterministic values so replays stay deterministic
- Tag-based querying — find runs by workflow class, tag, and status
- Artisan commands — list, inspect, signal, cancel, prune, and monitor runs
Defining Workflows and Actions
A workflow extends Workflow and calls action classes through $this->action(). Actions are resolved from the container, so dependencies are injected automatically:
use DiscoveryUkraine\SagaLaraFlow\Workflow;
class ProvisionAccountWorkflow extends Workflow
{
public function handle(string $email): array
{
$tenantId = $this->action(CreateTenant::class, $email)->run();
$this->action(SendWelcomeEmail::class, $email)->run();
return ['tenant' => $tenantId];
}
}
Runs are started via the SagaFlow facade. runSync() drives every step in-process, making it ideal for tests:
$run = SagaFlow::create(ProvisionAccountWorkflow::class)
->withArguments('jane@example.com')
->runSync();
$this->assertTrue($run->isCompleted());
Non-deterministic values like UUIDs must be wrapped in sideEffect() so replays always see the original result:
$reference = $this->sideEffect('reference', fn () => (string) Str::uuid());
Compensating Failed Transactions
Each step can register an undo action. If a later step fails, compensations fire in reverse order:
public function handle(string $orderId): void
{
$this->action(ChargeCard::class, $orderId)
->compensateWith(RefundCard::class, $orderId)
->run();
$this->action(ReserveStock::class, $orderId)
->compensateWith(ReleaseStock::class, $orderId)
->run();
// If this throws, ReleaseStock runs first, then RefundCard.
$this->action(ShipOrder::class, $orderId)->run();
}
For grouped rollbacks, $this->saga() supports onCompensationFailure() and compensateInParallel() to control whether a failed undo aborts the rollback and whether undos run concurrently.
Signals: Waiting on External Input
$this->signal() suspends a run until external code delivers the named signal. timeoutAfter() adds a deadline:
try {
$decision = $this->signal('approval')
->timeoutAfter(now()->addDay())
->wait();
} catch (AwaitSignalTimeoutException $e) {
$this->action(AutoReject::class)->run();
}
Deliver the signal from anywhere in your application:
SagaFlow::loadFlow($runId)->signal('approval', ['approved' => true]);
Tag-based querying lets you locate the right run without storing its ID:
SagaFlow::query()
->whereWorkflow(ProvisionCompanyWorkflow::class)
->whereTag('company', $companyId)
->signalable()
->handles()
->first()
?->signal('owner-synced');
Concurrency and Child Workflows
Independent actions run in parallel with $this->parallel():
[$pricing, $inventory, $reviews] = $this->parallel()
->action(FetchPricing::class, $sku)
->action(FetchInventory::class, $sku)
->action(FetchReviews::class, $sku)
->run();
Child workflows are invoked with $this->child(), and a ChildClosePolicy controls what happens when the parent finishes.
Installation
The package requires PHP 8.5 and Laravel 13:
composer require discovery-ukraine/saga-lara-flow
php artisan migrate
php artisan vendor:publish --tag="saga-lara-flow-config"
Register the expiration monitor with the Laravel scheduler:
Schedule::command('saga-flow:monitor')->everyMinute();
Key Takeaways
- Write multi-step distributed processes as a single readable
handle()method - Automatic replay skips already-completed steps without re-running side effects
- Compensating transactions roll back completed steps in reverse order on failure
- Signals pause execution until a human or external system responds
- Parallel blocks, optional steps, child workflows, and versioning cover advanced use cases
runSync()makes the whole workflow testable without a real queue
Full documentation is available at sagalaraflow.dev. Read the original announcement at Laravel News.