PHPackages                             discovery-ukraine/saga-lara-flow - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. discovery-ukraine/saga-lara-flow

ActiveLibrary[Queues &amp; Workers](/categories/queues)

discovery-ukraine/saga-lara-flow
================================

Simple workflow management engine with an integrated Saga pattern using Laravel Queues.

10PHPCI passing

Since Jul 1Pushed todayCompare

[ Source](https://github.com/discovery-ukraine/saga-lara-flow)[ Packagist](https://packagist.org/packages/discovery-ukraine/saga-lara-flow)[ RSS](/packages/discovery-ukraine-saga-lara-flow/feed)WikiDiscussions main Synced today

READMEChangelog (4)DependenciesVersions (1)Used By (0)

[   ![Saga Lara Flow](art/logo-light.png) ](https://sagalaraflow.dev)[![Latest Version on Packagist](https://camo.githubusercontent.com/17aa285ea1b7292c9a1568d94d8413e5ccfbcc9f9437bbebf587233e6b26eeaf/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f646973636f766572792d756b7261696e652f736167612d6c6172612d666c6f772e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/discovery-ukraine/saga-lara-flow)[![Tests](https://camo.githubusercontent.com/ad156b60320b876a70094af86bf6dcc5ce760ce4c521a6eac178f121de9a4592/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f646973636f766572792d756b7261696e652f736167612d6c6172612d666c6f772f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/discovery-ukraine/saga-lara-flow/actions/workflows/run-tests.yml)[![PHPStan](https://camo.githubusercontent.com/0515d7519753ba0a19866e8aa44825db1ca357c83cffe6bddeba58a56063ccef/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f646973636f766572792d756b7261696e652f736167612d6c6172612d666c6f772f7068707374616e2e796d6c3f6272616e63683d6d61696e266c6162656c3d7068707374616e267374796c653d666c61742d737175617265)](https://github.com/discovery-ukraine/saga-lara-flow/actions/workflows/phpstan.yml)

**Saga Lara Flow** is a workflow management engine with an integrated **Saga pattern**, built on top of Laravel Queues.

It lets you write a long-running, durable business process as a single deterministic PHP method: each step runs, is recorded, and survives worker restarts through exception-based suspension and replay. When a step fails partway through, registered **compensations** roll back the completed work in reverse order.

It is inspired by [Durable Workflow (formerly Laravel Workflow)](https://github.com/durable-workflow/workflow), but it is not a replacement for it. Saga Lara Flow positions itself as a lighter, native-Laravel alternative: no Fibers, generators, or promises — just queues, an event log, and Eloquent.

📚 **Full documentation: [sagalaraflow.dev](https://sagalaraflow.dev)**

```
use DiscoveryUkraine\SagaLaraFlow\Workflow;

class CheckoutWorkflow extends Workflow
{
    public function handle(string $orderId): array
    {
        $charge = $this->action(ChargeCard::class, $orderId)
            ->compensateWith(RefundCard::class, $orderId)
            ->run();

        $this->action(ReserveStock::class, $orderId)
            ->compensateWith(ReleaseStock::class, $orderId)
            ->run();

        $this->action(ShipOrder::class, $orderId)->run();

        return ['charge' => $charge];
    }
}
```

```
use DiscoveryUkraine\SagaLaraFlow\Facades\SagaFlow;

$run = SagaFlow::create(CheckoutWorkflow::class)
    ->withArguments('order-42')
    ->withTags(['tenant' => 'acme', 'order' => 'order-42'])
    ->run(); // dispatched onto the queue
```

If `ReserveStock` or `ShipOrder` fails, `RefundCard` (and any other registered compensation) runs automatically, in reverse order.

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Configuration](#configuration)
- [Your first workflow](#your-first-workflow)
- [Actions](#actions)
- [Sagas &amp; compensations](#sagas--compensations)
- [Signals](#signals)
- [Side effects](#side-effects)
- [Parallel actions](#parallel-actions)
- [Optional actions](#optional-actions)
- [Child workflows](#child-workflows)
- [Tags &amp; querying](#tags--querying)
- [Expiration &amp; monitoring](#expiration--monitoring)
- [Queues, locks &amp; idempotency](#queues-locks--idempotency)
- [Synchronous execution](#synchronous-execution)
- [Versioning long-running workflows](#versioning-long-running-workflows)
- [Octane &amp; multi-tenancy](#octane--multi-tenancy)
- [Determinism rules](#determinism-rules)
- [Events](#events)
- [Artisan commands](#artisan-commands)
- [Testing your workflows](#testing-your-workflows)

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require discovery-ukraine/saga-lara-flow
```

Run the migrations:

```
php artisan migrate
```

The engine's migration ships with the package, so `migrate` picks it up directly — no publish step. Future versions add their migrations the same way: `composer update` then `php artisan migrate`.

Optionally publish the config file:

```
php artisan vendor:publish --tag="saga-lara-flow-config"
```

Customize the schema through config — `database.table_prefix`, `database.connection`, and the swappable `models.*` — rather than by editing the migration. (The migration runs automatically; do not also `vendor:publish` it, or `migrate` would try to run both the published copy and the package's own.)

**Requirements:** PHP `^8.5`, Laravel 13 (`illuminate/*: ^13`). The package registers its service provider and the `SagaFlow` facade automatically.

Configuration
-------------

[](#configuration)

Every setting lives in `config/saga-lara-flow.php`. The most common ones:

- **Dedicated database connection.** `database.connection` (env `SAGA_LARA_FLOW_DB_CONNECTION`) keeps the engine's tables on their own connection; `null` uses the app default. `database.table_prefix`(default `saga_`) prefixes every table.
- **Swappable models.** Every row model under `models.*` can be pointed at your own subclass.
- **Queue.** `queue.connection` / `queue.queue` control where workflow and action jobs run; `queue.after_commit` dispatches after the surrounding DB transaction commits.
- **Locks.** `locks.*` configure the `WithoutOverlapping` middleware that serializes concurrent drives of a single run. `workflow_ttl_seconds` / `action_ttl_seconds` / `block_seconds` are in seconds. See [Queues, locks &amp; idempotency](#queues-locks--idempotency).
- **Monitor.** `monitor.expiration.defaults` set implicit deadlines (seconds) for `run` / `action` / `signal` — `null` means no default. See [Expiration &amp; monitoring](#expiration--monitoring).
- **Sagas / parallel / children.** Default compensation, failure, and close policies.
- **Tenancy.** `tenancy.*` callable hooks — see [Octane &amp; multi-tenancy](#octane--multi-tenancy).

Your first workflow
-------------------

[](#your-first-workflow)

Generate a workflow and an action:

```
php artisan make:workflow ProvisionAccountWorkflow
php artisan make:action  CreateTenant
```

A **workflow** is a class extending `Workflow` with a deterministic `handle()`. A **workflow author**never `new`s an action — they schedule it through the DSL, and the engine runs, records, and replays it for you:

```
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];
    }
}
```

An **action** is a unit of work with native Laravel dependency injection:

```
use DiscoveryUkraine\SagaLaraFlow\Action;

class CreateTenant extends Action
{
    public function handle(TenantRepository $tenants, string $email): string
    {
        return $tenants->provision($email)->id;
    }
}
```

Start it:

```
use DiscoveryUkraine\SagaLaraFlow\Facades\SagaFlow;

$run = SagaFlow::create(ProvisionAccountWorkflow::class)
    ->withArguments('jane@example.com')
    ->run();          // queued; returns a Pending FlowRun immediately

// or run it inline, driving every step in-process:
$run = SagaFlow::create(ProvisionAccountWorkflow::class)
    ->withArguments('jane@example.com')
    ->runSync();      // returns a Completed FlowRun
```

Actions
-------

[](#actions)

`action(string $actionClass, mixed ...$arguments)` returns an `ActionBuilder`; `run()` executes it and returns the action's result. Arguments passed here are forwarded to the action's `handle()`after its injected dependencies.

**Retries and timeouts** use native Laravel queue semantics — declare them on the action:

```
class ChargeCard extends Action
{
    public int $tries = 3;    // up to 3 attempts when queued
    public int $timeout = 30; // seconds per attempt

    public function handle(PaymentGateway $gateway, string $orderId): string
    {
        return $gateway->charge($orderId);
    }
}
```

A **per-step deadline** (independent of the queue timeout) is set on the builder:

```
$this->action(ChargeCard::class, $orderId)
    ->expiresAt(now()->addMinutes(2))
    ->run();
```

`run()` throws when the action ultimately fails, so a workflow can react to it:

```
use DiscoveryUkraine\SagaLaraFlow\Exceptions\ActionFailedException;
use DiscoveryUkraine\SagaLaraFlow\Exceptions\FlowExpiredException;

try {
    $this->action(ChargeCard::class, $orderId)->run();
} catch (ActionFailedException $e) {
    // retries exhausted — decide what the workflow does next
} catch (FlowExpiredException $e) {
    // the step (or run) passed its deadline
}
```

> ⚠️ Never catch `DiscoveryUkraine\SagaLaraFlow\Exceptions\Internal\FlowSuspended` (or any `InternalFlowControl`) — those are the engine's suspend/replay signals, not errors. If you use a broad `catch (\Throwable $e)`, re-throw control flow first: `if ($this->isFlowControl($e)) { throw $e; }`.

Sagas &amp; compensations
-------------------------

[](#sagas--compensations)

The Saga pattern trades distributed transactions for **compensating actions**: each step registers how to undo itself, and on failure the engine rolls completed steps back in reverse order.

**Action-level compensation** (the primary style) attaches an undo to each step:

```
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 then RefundCard run automatically.
    $this->action(ShipOrder::class, $orderId)->run();
}
```

Compensation can also be a closure:

```
$this->action(MakeReservation::class, $id)
    ->compensateWith(fn () => Reservation::release($id))
    ->run();
```

**Grouped sagas** via `saga()` express a compensation boundary explicitly and give you group-level policies:

```
use DiscoveryUkraine\SagaLaraFlow\Enums\CompensationFailurePolicy;

$this->saga()
    ->onCompensationFailure(CompensationFailurePolicy::Continue) // keep rolling back even if one undo fails
    ->compensateInParallel()                                     // run the group's undos concurrently
    ->step(ChargeCard::class, $orderId)->compensateWith(RefundCard::class, $orderId)
    ->step(ReserveStock::class, $orderId)->compensateWith(ReleaseStock::class, $orderId)
    ->run();
```

`CompensationFailurePolicy::Stop` (default) halts the rollback on the first failed compensation; `Continue` presses on. Precedence for policies is **action &gt; group &gt; config**. If a compensation itself fails under `Stop`, a `CompensationFailedException` surfaces.

Signals
-------

[](#signals)

Signals let external code push data or decisions into a running workflow. Inside `handle()`, `awaitSignal()` blocks the workflow (by suspending it) until the named signal arrives, then returns its payload:

```
public function handle(): void
{
    $decision = $this->awaitSignal('approval');           // suspends until delivered

    if (($decision['approved'] ?? false) === true) {
        $this->action(Publish::class)->run();
    }
}
```

A **timeout** turns an unanswered wait into a catchable exception:

```
use DiscoveryUkraine\SagaLaraFlow\Exceptions\AwaitSignalTimeoutException;

try {
    $decision = $this->signal('approval')
        ->timeoutAfter(now()->addDay())
        ->wait();
} catch (AwaitSignalTimeoutException $e) {
    $this->action(AutoReject::class)->run();
}
```

Deliver a signal from anywhere via the handle:

```
SagaFlow::loadFlow($runId)->signal('approval', ['approved' => true]);

// safe variant that returns false instead of throwing on a terminal run:
SagaFlow::loadFlow($runId)->signalIfRunning('approval', ['approved' => true]);
```

Side effects
------------

[](#side-effects)

Anything non-deterministic (random values, `now()`, a UUID, an external read) must be wrapped in `sideEffect()` so replay reuses the **recorded** value instead of computing a new one:

```
public function handle(): void
{
    $reference = $this->sideEffect('reference', fn () => (string) Str::uuid());

    $this->action(CreateInvoice::class, $reference)->run();
}
```

The first execution records the value; every later replay of the run returns the same stored value.

Parallel actions
----------------

[](#parallel-actions)

`parallel()` runs several actions concurrently (as queued jobs, or inline under `runSync`) and returns their results as a list:

```
use DiscoveryUkraine\SagaLaraFlow\Enums\ParallelFailurePolicy;

[$a, $b, $c] = $this->parallel()
    ->action(FetchPricing::class, $sku)
    ->action(FetchInventory::class, $sku)
    ->action(FetchReviews::class, $sku)
    ->failFast()          // cancel the block on the first failure (default)
    ->run();
```

`->waitAllThenFail()` lets every step settle before the block fails; `failFast()` (the config default, `ParallelFailurePolicy::FailFast`) short-circuits on the first hard failure. Steps in a parallel block can carry their own compensations and `optionalAction()`.

Optional actions
----------------

[](#optional-actions)

An optional action never fails the flow — its failure is swallowed and a fallback is returned:

```
$score = $this->action(FetchRiskScore::class, $orderId)
    ->continueOnFailure()
    ->fallbackValueOnFail(0)
    ->run();

// shorthand:
$score = $this->optionalAction(FetchRiskScore::class, $orderId)
    ->fallbackValueOnFail(0)
    ->run();
```

You can also mark it declaratively with `#[ContinueOnFailure]` on the action class.

Child workflows
---------------

[](#child-workflows)

A workflow can start another workflow and await its result. The child inherits the parent's connection, queue, and **tenant context**:

```
use DiscoveryUkraine\SagaLaraFlow\Enums\ChildClosePolicy;

public function handle(): array
{
    $result = $this->child(ShipmentWorkflow::class, ['order-42'])
        ->closePolicy(ChildClosePolicy::Cancel) // what happens to the child if the parent closes
        ->run();

    return ['shipment' => $result];
}
```

Close policies: `Abandon` (default — leave the child running), `Cancel` (cancel it), `Fail` (fail it). A failing child throws `ChildWorkflowFailedException` (or `ChildWorkflowCancelledException`) unless you call `->continueParentOnFailure()`. The default close policy is configurable (`children.default_close_policy`) or per class via `#[ChildPolicy]`.

Tags &amp; querying
-------------------

[](#tags--querying)

Attach searchable key/value tags at creation or from inside the workflow:

```
SagaFlow::create(CheckoutWorkflow::class)
    ->withTags(['tenant' => 'acme', 'channel' => 'web'])
    ->run();

// inside handle():
$this->tag('priority', 'high');
```

Query runs with the fluent, type-safe `FlowQuery`:

```
use DiscoveryUkraine\SagaLaraFlow\Enums\FlowStatus;

$stuck = SagaFlow::query()
    ->whereWorkflow(CheckoutWorkflow::class)
    ->whereTag('tenant', 'acme')
    ->waiting()
    ->before(now()->subHour())
    ->get();               // Collection

$handles = SagaFlow::query()->running()->handles();   // Collection
$count   = SagaFlow::query()->failed()->count();
```

Terminals: `get()`, `first()`, `count()`, `paginate()`, `handles()`, and `builder()` (the raw Eloquent builder for ordering/limits).

Expiration &amp; monitoring
---------------------------

[](#expiration--monitoring)

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

**Scheduler (recommended).** Run the monitor on a schedule:

```
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 by enabling `monitor.queue_looping.enabled` (throttled by `throttle_seconds`). Useful when you have no cron but always-on workers.

For runs whose progress was lost to a *dropped job* (rather than a deadline), the **doctor** can re-dispatch missing actions (`repair.redispatch_lost_actions`) and re-wake stuck waits (`repair.wake_stuck_flows`) — enable `repair.enabled` and either schedule `saga-flow:repair` or loop it off the worker (`repair.queue_looping.enabled`), or kick a single run manually with `saga-flow:kick {run}` / `SagaFlow::kick($id)`. Each config key is documented in [Expiration &amp; monitoring](https://sagalaraflow.dev/expiration-and-monitoring).

Queues, locks &amp; idempotency
-------------------------------

[](#queues-locks--idempotency)

Every workflow and action runs as a queued job on the configured connection/queue. A run is driven by replaying `handle()` from the recorded history; each operation is identified by a deterministic `(flow_run_id, sequence)` pair, so a step that has **completed and recorded its result** is never repeated — it is reused from history. The `WithoutOverlapping` locks (`locks.*`, TTLs and waits in seconds) serialize concurrent drives of the same run so two workers can't advance it at once.

This is *not* automatic end-to-end idempotency. The reuse guarantee covers **recorded** steps only — it does not make the work *inside* an action idempotent. If a job hangs, is retried, or dies after performing its external effect (charging a card, calling an API) but before recording its result, that effect can happen more than once. End-to-end idempotency depends on your action code: use an idempotency key, prefer upserts, or check whether the effect already happened. The `(flow_run_id, sequence)` pair makes a stable idempotency key to hand downstream. See [Queues, locks &amp; idempotency](https://sagalaraflow.dev/queues-locks-idempotency).

Synchronous execution
---------------------

[](#synchronous-execution)

`runSync()` drives the whole workflow in-process, using the same single replay loop as the queued path — handy for tests, tinkering, or short workflows:

```
$run = SagaFlow::create(CheckoutWorkflow::class)
    ->withArguments('order-42')
    ->runSync();

$run->status;   // FlowStatus::Completed
$run->result;   // the array handle() returned
```

The queued and synchronous paths are guaranteed to reach the **same** final database state.

Versioning long-running workflows
---------------------------------

[](#versioning-long-running-workflows)

A workflow may be suspended for days while its code keeps shipping. To change a running workflow's logic without breaking in-flight runs, keep versions in separate classes/directories (`App\Workflows\V1\CheckoutWorkflow`, `App\Workflows\V2\CheckoutWorkflow`) and pin a version at creation:

```
SagaFlow::create(\App\Workflows\V2\CheckoutWorkflow::class)
    ->version('v2')
    ->run();
```

Read the pinned version inside `handle()` with `$this->version()`; existing runs keep replaying against the class they were created with.

Octane &amp; multi-tenancy
--------------------------

[](#octane--multi-tenancy)

The engine runs each workflow/action `handle()` in the tenant the run was **created** for and reverts afterwards, so nothing leaks between runs on a shared Octane or queue worker.

- **Capture at creation.** `SagaFlow::create(...)` snapshots the current tenant via the `tenancy.capture` hook onto `flow_runs.tenancy_context`. Child runs inherit the parent's context.
- **Auto-restore is opt-in.** Off by default (`tenancy.auto`). When on, the engine calls `tenancy.restore` before `handle()` and reverts in a `finally` (via `tenancy.end`, or by restoring the previous context). Override per class with `#[Tenancy(auto: true)]` (precedence: attribute &gt; config).
- **Manual discovery.** Even with auto off, read the run's tenant inside `handle()`: `SagaFlow::tenancyContext()` returns `['tenant' => '…']` or `null`.

```
// config/saga-lara-flow.php
'tenancy' => [
    'auto'    => false,
    'capture' => fn (): array => ['tenant' => tenant()?->getTenantKey()],
    'restore' => fn (array $c): void => tenancy()->initialize($c['tenant']),
    'end'     => null, // optional explicit revert; otherwise the previous context is restored
],
```

See the [multi-tenancy docs](https://sagalaraflow.dev/octane-and-multi-tenancy) for a full stancl/tenancy integration example.

Determinism rules
-----------------

[](#determinism-rules)

`handle()` is **replayed** from the start on every resume, so it must be deterministic:

- ✅ Do call actions, child workflows, signals, and parallel blocks through the DSL — their results are recorded and reused on replay.
- ✅ Do wrap any nondeterminism (`now()`, random, UUIDs, direct DB/HTTP reads) in `sideEffect()`.
- ❌ Don't branch on ambient state that can change between replays (wall-clock time, `rand()`, external reads) outside a `sideEffect()`.
- ❌ Don't catch the engine's control-flow exceptions (`FlowSuspended`) as if they were errors.

Break a rule and the history contract guard raises `HistoryContractMismatchException` when the replay diverges from the recorded history.

Events
------

[](#events)

The engine mirrors its `flow_events` log onto Laravel events you can listen to — e.g. `FlowStarted`, `FlowCompleted`, `FlowFailed`, `FlowWaiting`, `FlowCancelled`, `ActionCompleted`, `ActionFailed`, `CompensationCompleted`, `ChildWorkflowCompleted`, `SideEffectRecorded`, and more (see `src/Events`). Register listeners as usual:

```
use DiscoveryUkraine\SagaLaraFlow\Events\FlowFailed;

Event::listen(FlowFailed::class, function (FlowFailed $event): void {
    report($event->flowRun->workflow_class.' failed: '.$event->flowRun->id);
});
```

`FlowCancelled` carries an optional `?string $reason`, populated when you call `$handle->cancel('reason here')`.

Artisan commands
----------------

[](#artisan-commands)

CommandPurpose`saga-flow:list {--status=} {--tag=} {--workflow=} {--limit=50}`List runs, newest first, with filters.`saga-flow:show {run} {--compact}`Inspect a run: header, actions, signals, compensations, history.`saga-flow:signal {run} {name} {--payload=}`Deliver a JSON-payload signal and wake the run.`saga-flow:cancel {run} {--compensate}`Cancel a non-terminal run; `--compensate` rolls back first.`saga-flow:kick {run}`Manually re-drive a stuck run.`saga-flow:monitor`Expire overdue runs/actions and time out waits.`saga-flow:repair`Recover runs whose progress was lost to a dropped job.`saga-flow:prune {--days=} {--before=} {--dry-run}`Delete old terminal runs and related rows.`make:workflow {name}`Generate a workflow class in `App\Workflows`.`make:action {name}`Generate an action class in `App\Actions`.These are CLI only — the package exposes no HTTP routes.

Testing your workflows
----------------------

[](#testing-your-workflows)

Under test, the **queued** paths must run against a real database queue driven with `queue:work --stop-when-empty` — the `sync` driver bypasses the suspend/replay machinery and won't exercise the engine faithfully. `runSync()` is fine for asserting final state directly:

```
$run = SagaFlow::create(CheckoutWorkflow::class)->withArguments('order-1')->runSync();

expect($run->status)->toBe(FlowStatus::Completed)
    ->and($run->result)->toBe(['charge' => 'ch_123']);
```

For queued assertions, set the queue to the `database` connection, dispatch with `->run()`, then drain the queue before asserting. The package's own suite (`tests/`) is a working reference.

```
composer test        # Pest
composer analyse     # PHPStan (larastan, level 5)
composer lint        # Pint + PHPStan
```

When should I use Durable Workflow instead?
-------------------------------------------

[](#when-should-i-use-durable-workflow-instead)

Saga Lara Flow is intentionally a lighter, Laravel-native package. It is focused on queues, Eloquent, an event log, replay, signals, child workflows, and first-class Saga compensations inside a single Laravel application.

If you need a more complete workflow engine — SDK-neutral or polyglot workers, standalone/external workers, Fiber-based execution, strict workflow-definition fingerprinting, worker compatibility fleets, sticky execution, durable timers, schedules, control-plane APIs, rich projections/observability, search attributes, memos, history export/import, replay verification, external payload storage, history budgets, or Temporal/Cadence-style operations — you should evaluate [Durable Workflow](https://github.com/durable-workflow/workflow) instead.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](SECURITY.md) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Andriy Karpishyn](https://github.com/mkrnnk)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

21

↑

LowBetter than 18% of packages

Maintenance65

Regular maintenance activity

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 96% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/5f17940e269a875476d6dc0cc072ebc416e6ef5869807e6f8952b75537bf9b8a?d=identicon)[makaronnik](/maintainers/makaronnik)

---

Top Contributors

[![discovery-ukraine](https://avatars.githubusercontent.com/u/9981799?v=4)](https://github.com/discovery-ukraine "discovery-ukraine (24 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

### Embed Badge

![Health badge](/badges/discovery-ukraine-saga-lara-flow/health.svg)

```
[![Health](https://phpackages.com/badges/discovery-ukraine-saga-lara-flow/health.svg)](https://phpackages.com/packages/discovery-ukraine-saga-lara-flow)
```

###  Alternatives

[league/geotools

Geo-related tools PHP 7.3+ library

1.4k5.6M31](/packages/league-geotools)[illuminate/bus

The Illuminate Bus package.

6046.3M531](/packages/illuminate-bus)[uecode/qpush-bundle

Asynchronous processing for Symfony using Push Queues

1672.5M2](/packages/uecode-qpush-bundle)[mayconbordin/l5-stomp-queue

Stomp Queue Driver for Laravel 5

121.1k](/packages/mayconbordin-l5-stomp-queue)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
