PHPackages                             padosoft/laravel-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. [Utility &amp; Helpers](/categories/utility)
4. /
5. padosoft/laravel-flow

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

padosoft/laravel-flow
=====================

DX-first workflow / saga / compensation engine for Laravel with native dry-run, reverse-order compensation, business impact tracking, opt-in persistence, and audit events.

v1.1.1(1mo ago)210.1k↓29.4%2Apache-2.0PHPPHP ^8.3CI passing

Since May 2Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/padosoft/laravel-flow)[ Packagist](https://packagist.org/packages/padosoft/laravel-flow)[ RSS](/packages/padosoft-laravel-flow/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (4)Dependencies (20)Versions (16)Used By (2)

laravel-flow
============

[](#laravel-flow)

[![Tests](https://github.com/padosoft/laravel-flow/actions/workflows/ci.yml/badge.svg)](https://github.com/padosoft/laravel-flow/actions/workflows/ci.yml)[![Latest Version](https://camo.githubusercontent.com/aef69aafed620162f408adeaa506001d1a7ce34cf62bd054b0c6900fb529cafc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7061646f736f66742f6c61726176656c2d666c6f772e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/padosoft/laravel-flow)[![PHP Version](https://camo.githubusercontent.com/eb676c7037f499c633955e534f474e8754a6a01ec690d41db63a3981c4d71a20/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f7061646f736f66742f6c61726176656c2d666c6f772e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/padosoft/laravel-flow)[![Laravel Version](https://camo.githubusercontent.com/65b75f1cb51a39d6e5d141772ac32dfe3641acf38ff29dc42115f4d0862af45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31332e782d7265643f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c)](https://laravel.com)[![License](https://camo.githubusercontent.com/a39964403b3ad23090c20e0f4e86b834c7f13cd17f0fb9e042c3cce06f6a13b4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4170616368652d2d322e302d626c75652e7376673f7374796c653d666c61742d737175617265)](LICENSE)[![Total Downloads](https://camo.githubusercontent.com/d117578532b91b4357f65f5e156ba48ae58d20554e555a56e52e4b1df6037b53/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7061646f736f66742f6c61726176656c2d666c6f772e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/padosoft/laravel-flow)

[![Laravel Flow banner](resources/laravel-flow-banner.png)](resources/laravel-flow-banner.png)

> **DX-first workflow / saga / compensation engine for Laravel — with native dry-run, configurable compensation strategies, business-impact projection, opt-in persistence, and audit events. Built for Laravel teams that need dry-run, compensation, and persisted run telemetry inside the app they already operate.**

Official documentation:

`laravel-flow` is the third deliverable of the [Padosoft v4.0 cycle](https://github.com/lopadova/AskMyDocs) (W5). It is a community Apache-2.0 package, **standalone-agnostic** (zero references to AskMyDocs / sister packages), and ships with the Padosoft AI vibe-coding pack so you can extend it with Claude Code or GitHub Copilot in minutes — not days.

```
use Padosoft\LaravelFlow\Facades\Flow;

Flow::define('promotion.create')
    ->withInput(['brand', 'discount_pct', 'starts_at', 'ends_at'])
    ->step('validate', ValidatePromotionInput::class)
    ->step('simulate', SimulatePromotionImpact::class)
        ->withDryRun(true)
    ->step('persist', PersistPromotion::class)
        ->compensateWith(ReversePromotion::class)
    ->register();

$run    = Flow::execute('promotion.create', $input);   // real execution
$dryRun = Flow::dryRun ('promotion.create', $input);   // simulate, no writes
```

---

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

[](#table-of-contents)

- [Why this package](#why-this-package)
- [Design rationale](#design-rationale)
- [Features at a glance](#features-at-a-glance)
- [Web admin UI](#web-admin-ui)
- [Comparison vs alternatives](#comparison-vs-alternatives)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Usage examples](#usage-examples)
- [Configuration reference](#configuration-reference)
- [Architecture](#architecture)
- [AI vibe-coding pack](#ai-vibe-coding-pack)
- [Testing — Default + Live](#testing--default--live)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)

---

Why this package
----------------

[](#why-this-package)

Laravel applications routinely need to orchestrate **multi-step business workflows** that mix:

- **Validations** (some safe to skip, some load-bearing).
- **Simulations** (project the impact of an operation without writing).
- **Manual sign-off checkpoints** (v0.3 approval gates can now pause, resume, or reject persisted runs; CLI approvals and signed webhook outbox delivery are available).
- **Side-effecting writes** (DB rows, queue jobs, vendor API calls).
- **Compensation chains** (when step N fails, undo step N-1 ... step 1).
- **Audit trails** (regulators want to see *who did what, when, with which inputs, in which order*).

The Laravel ecosystem has plenty of tools for *some* of these — `Bus::chain()` for sequence, jobs for async, `transaction()` for atomicity — but none of them ship with **native dry-run**, **reverse-order saga compensation**, and **a single fluent surface** that a junior dev can read in 30 seconds.

`laravel-flow` is that surface.

It is **deliberately small**. v0.1 is in-memory, synchronous, container-resolved. The current v0.2 foundation adds opt-in DB persistence for runs, steps, and audit rows plus queued dispatch, guarded retry metadata, database queue coverage, terminal-run replay, and opt-in parallel compensation for independent compensators. v0.3 has the approval pause primitive, hashed one-time approval-token issuance, persisted `Flow::resume()` / `Flow::reject()` APIs, CLI approvals, and signed webhook outbox delivery. The current v1.0 macro ships the package-side **dashboard contracts** (`FlowDashboardReadModel` read service, `DashboardActionAuthorizer` authorization hook with `DenyAllAuthorizer` default) so a separate companion app (`padosoft-laravel-flow-dashboard`, see [`docs/DASHBOARD_APP_SPEC.md`](docs/DASHBOARD_APP_SPEC.md)) can build the operator UI on a stable headless surface.

---

Design rationale
----------------

[](#design-rationale)

Five non-negotiable choices that drove the API:

### 1. Dry-run is a first-class flag, not a convention

[](#1-dry-run-is-a-first-class-flag-not-a-convention)

Every handler can declare `->withDryRun(true)`. When the engine runs in dry mode, it **invokes** dry-run-aware steps (so they can project impact) and **skips** the others, returning `dry_run_skipped` markers. There is no separate "preview" code path to maintain.

### 2. Compensation walks backwards by default

[](#2-compensation-walks-backwards-by-default)

Saga semantics: when step N fails, the engine walks the previously-completed steps from N-1 back to 1, calling each registered `FlowCompensator`. There is no "compensate forward" or "best-effort cleanup" mode — predictable rollback every time.

When all compensators for a flow are independent and idempotent, set `compensation_strategy=parallel` to batch completed compensators through Laravel Concurrency. Reverse order stays the default because many saga rollbacks depend on undoing the newest side effect first. If the engine is constructed with an isolated/non-global container, or the configured Concurrency driver cannot run, parallel strategy falls back to in-process compensation through the injected container.

### 3. Handlers and compensators are container-resolved classes

[](#3-handlers-and-compensators-are-container-resolved-classes)

`step('persist', PersistPromotion::class)` not `step('persist', fn () => ...)`. Closures don't survive serialization (queued workers in v0.2), don't get DI, and don't surface in stack traces. Class-based handlers cost one extra file and pay back tenfold in observability.

### 4. The audit trail is event-driven

[](#4-the-audit-trail-is-event-driven)

When `audit_trail_enabled` is enabled, normal-case step and compensation transitions dispatch the matching Laravel event, such as `FlowStepStarted`, `FlowStepCompleted`, `FlowStepFailed`, or `FlowCompensated`. When persistence is enabled, step events are dispatched only after the matching audit append succeeds, and compensation events are skipped if their audit append fails. The host application subscribes once and routes those events to the logger, DB, or metrics backend it already runs. Persisted `flow_audit` rows are written only for non-dry-run executions when both persistence and `audit_trail_enabled` are enabled. Dry-runs never write run, step, or audit rows.

### 5. Standalone-agnostic — zero AskMyDocs symbols

[](#5-standalone-agnostic--zero-askmydocs-symbols)

`laravel-flow` is a **community** package. It is not coupled to AskMyDocs, the sister patent-box-tracker, or any other Padosoft project. An architecture test enforces this on every CI run by walking `src/` with `RecursiveDirectoryIterator` and asserting forbidden substrings never appear.

---

Features at a glance
--------------------

[](#features-at-a-glance)

- **Fluent definition builder** — `Flow::define($name)->withInput([...])->step(...)->register()`.
- **Native dry-run** — `Flow::dryRun($name, $input)` simulates without persisting; supporting handlers project impact, others self-skip.
- **Configurable saga compensation** — `compensateWith(Compensator::class)` per step; default reverse-order rollback, plus opt-in `parallel` batching for independent compensators.
- **Audit events and persisted audit rows** — normal-case transitions dispatch matching `FlowStep*` / `FlowCompensated` events when `audit_trail_enabled=true`; persisted `flow_audit` rows are written only for non-dry-run executions with both `persistence.enabled=true` and `audit_trail_enabled=true`, and those rows are append-only during normal runtime but retention-prunable with `flow:prune`.
- **Business-impact projection** — handlers return `businessImpact: [...]` alongside output, surfaced on every step result.
- **Opt-in persisted execution** — `flow_runs`, `flow_steps`, and `flow_audit` migrations, Eloquent repositories, immutable run identity updates, correlation/idempotency keys, transaction-scoped step transitions, atomic step upserts, compensate-first runtime-abort recovery, sanitized listener/error storage, clock-aware audit timestamps, redacted JSON payload storage, and retention pruning.
- **Queued dispatch foundation** — `Flow::dispatch($name, $input, $options)` validates the flow and queues an after-commit `RunFlowJob` with a per-dispatch cache lock plus guarded Laravel-native tries/backoff metadata; sync and database queue paths have package coverage.
- **Terminal-run replay** — `php artisan flow:replay {runId}` creates a new persisted run linked to the original via `replayed_from_run_id` and warns when the current registered definition drifted from stored step metadata.
- **Approval gate pause state** — `approvalGate($name)` adds a built-in dry-run-aware step that marks the run `paused`, emits/persists `FlowPaused`, and, when persistence is enabled, issues a pending approval record.
- **Persisted approval resume/reject API** — `Flow::resume($token, $payload, $actor)` consumes the pending token only while the run is still paused, marks the approval gate succeeded, reconstructs persisted context, and continues downstream steps under a per-run shared cache lock; retries skip downstream steps that already have persisted success rows. `Flow::reject($token, $payload, $actor)` fails the gate and compensates prior completed steps. Custom persistence backends opt into this API by binding their approval backend to `ApprovalRepository`, implementing `ApprovalDecisionRepository` on that approval backend, returning a `ConditionalRunRepository` from `FlowStore::runs()`, and applying the package payload redactor consistently for approval decision JSON.
- **Hashed approval-token foundation** — `ApprovalTokenManager` issues expiring one-time approval records, persists only SHA-256 token hashes, and consumes approve/reject decisions with redacted actor metadata; the plain token is available only from the returned run at issuance time, while event/audit payloads carry non-secret metadata.
- **Signed webhook outbox delivery** — lifecycle rows for `flow.completed`, `flow.failed`, `flow.paused`, and `flow.resumed` are persisted with attempt counts and retry scheduling; `php artisan flow:deliver-webhooks` signs every payload with an `X-Laravel-Flow-Signature` header and reschedules transient failures up to a configured retry limit.
- **Headless dashboard contracts** — `FlowDashboardReadModel` exposes paginated `listRuns`, `findRun`, `listApprovals`, `pendingApprovals`, `listWebhookOutbox`, and aggregated `kpis` returning immutable DTOs; `DashboardActionAuthorizer` is the host-app authorization hook with `DenyAllAuthorizer` registered as the deny-by-default binding (`AllowAllAuthorizer` ships as an explicit dev opt-in). The companion app spec is at [`docs/DASHBOARD_APP_SPEC.md`](docs/DASHBOARD_APP_SPEC.md).
- **Container-resolved handlers** — full DI, type hints, and stack traces.
- **Strict input validation** — `withInput(['a','b'])` throws `FlowInputException` if a key is missing.
- **Parallel compensation strategy** — `compensation_strategy=parallel` batches completed compensators through Laravel Concurrency when compensators are safe to run without reverse-order dependencies.
- **Testbench-friendly** — TestCase + stubs ready to copy.
- **🚀 AI vibe-coding pack included** — `.claude/` directory with skills, rules, agents, commands, and the Padosoft Copilot review loop pre-wired.
- **PHP 8.3 / 8.4 / 8.5 × Laravel 13** matrix on every CI run.

---

Web admin UI
------------

[](#web-admin-ui)

A full-featured web admin panel for `laravel-flow` is available at [`padosoft/laravel-flow-admin`](https://github.com/padosoft/laravel-flow-admin).

[![Laravel Flow admin dashboard](resources/screenshoots/laravel-flow-admin-dashboard.png)](resources/screenshoots/laravel-flow-admin-dashboard.png)

---

Comparison vs alternatives
--------------------------

[](#comparison-vs-alternatives)

Legend: `✅ YES` means the capability is first-class in the current product, `⚠️ PARTIAL` means it is possible but manual, narrower, or provided through a different model, and `❌ NO` means it is not available today.

Feature`laravel-flow`Durable Workflow (Laravel)Symfony WorkflowTemporalAWS Step FunctionsNative dry-run with no persistence writes✅ YES - first-class `Flow::dryRun()`; no run, step, audit, or compensator writes❌ NO - not documented as a first-class mode❌ NO - app must model preview behavior❌ NO - app must model simulation separately❌ NO - app must model simulation separatelyReverse-order saga compensation✅ YES - built-in per-step `compensateWith()` with default reverse-order rollback and opt-in `parallel` batching for independent compensators⚠️ PARTIAL - sagas/error handling are possible, but compensation policy is workflow-defined⚠️ PARTIAL - manual transition/state design⚠️ PARTIAL - compensation pattern via workflow code/SDKs⚠️ PARTIAL - manual cleanup states via `Catch`Approval gate as a step type✅ YES - `approvalGate($name)` pauses and audits runs, issues hashed expiring token records, reissues a later paused gate token once when an older approved token retries, and per-run shared-lock-guarded `Flow::resume()` / `Flow::reject()` continue or compensate persisted runs; duplicate resumes return the current persisted `running` state instead of re-entering handlers; `flow:approve` and `flow:reject` CLI commands cover operator decisions⚠️ PARTIAL - model manually as a long-running workflow/activity⚠️ PARTIAL - guards can block transitions but are not resumable approval steps⚠️ PARTIAL - signals/await patterns, not Laravel step gates✅ YES - callback/task-token patternSigned webhook outbox delivery✅ YES - `flow_webhook_outbox` rows for `flow.paused/resumed/completed/failed` written inside engine transactions; `flow:deliver-webhooks` signs payloads with HMAC-SHA256 in an `X-Laravel-Flow-Signature: t=...,v1=...` header, leases pending/stale rows with attempts compare-and-set guard, and reschedules transient failures with exponential backoff up to a configured max❌ NO - not documented as built-in❌ NO - not part of the workflow component⚠️ PARTIAL - signed delivery via custom activities or external integration code, not first-class⚠️ PARTIAL - integrate with EventBridge/SNS/Lambda; signing is consumer-sideHeadless dashboard contracts✅ YES - package-side `FlowDashboardReadModel` (paginated `listRuns`/`findRun`/`listApprovals`/`listWebhookOutbox`/`kpis` returning immutable DTOs) plus `DashboardActionAuthorizer` interface bound to `DenyAllAuthorizer` by default; companion app brief at `docs/DASHBOARD_APP_SPEC.md` keeps the package itself headless❌ NO - no read-model contract; consume Eloquent records directly❌ NO - app-defined, no read-model contract✅ YES - first-class Temporal Web UI plus gRPC/REST APIs (managed dashboard, not a Composer-path-repo headless contract)✅ YES - first-class AWS Console execution graph + history (managed dashboard, not a Composer-path-repo headless contract)Web admin UI✅ YES - companion admin panel available at [`padosoft/laravel-flow-admin`](https://github.com/padosoft/laravel-flow-admin) for runs, approvals, failures, outbox, and operational KPIs⚠️ PARTIAL - optional Waterline UI, tied to that package's durable workflow model❌ NO - no bundled admin panel✅ YES - Temporal Web UI for workflow executions and history✅ YES - AWS Console for state-machine executions and history`@api`/`@internal` source marking + contract-test pinning✅ YES - every public class is annotated `@api` (SemVer-covered) or `@internal` (implementation detail); `tests/Contract/PublicApiContractTest` pins class names, public methods, and constants so a follow-up patch cannot silently rename or remove the v1.0 surface❌ NO - no in-source visibility tags or contract-test pinning documented⚠️ PARTIAL - Symfony framework uses `@internal` annotations selectively; no companion contract-test pinning⚠️ PARTIAL - SDKs document stability but no Laravel-package-level contract test surface⚠️ PARTIAL - service-level API stability via AWS SLAs, not in-source per-class markersMigration guides from competitors✅ YES - `docs/MIGRATION_DURABLE.md` (Temporal-style mapping with trade-off table), `docs/MIGRATION_SYMFONY.md` (state-machine to linear-flow mapping with event listener table), `docs/UPGRADE.md` (v0.1→v1.0 + SemVer policy)❌ NO - not shipped❌ NO - not shipped⚠️ PARTIAL - upgrade docs between Temporal versions exist; no migration-from-competitors docs❌ NO - AWS migration tooling targets services, not other workflow enginesContainer-resolved PHP handlers✅ YES - handlers and compensators resolve through Laravel's container✅ YES - PHP workflow/activity classes✅ YES - Symfony services/listeners❌ NO - worker model is outside Laravel's container❌ NO - Lambda/service fanoutAudit trail and event hooks✅ YES - `FlowStep*` / `FlowCompensated` events plus optional `flow_audit` rows⚠️ PARTIAL - status tracking and Laravel event integration✅ YES - workflow events and optional audit trail✅ YES - managed workflow event history✅ YES - execution history plus CloudWatch/CloudTrail integrationsIn-memory default with opt-in app DB persistence✅ YES - memory by default; DB runs/steps/audit only when enabled❌ NO - durable persistence is central to the engine⚠️ PARTIAL - marking store is app-defined❌ NO - dedicated Temporal service/cluster❌ NO - managed AWS serviceRedacted JSON persistence✅ YES - configurable key redaction before run/step/audit payload storage❌ NO - not documented as built-in❌ NO - app-defined storage concern⚠️ PARTIAL - custom payload codecs/converters, not Laravel key config⚠️ PARTIAL - service-level data handling, not Laravel key configCorrelation and idempotency keys✅ YES - first-class `FlowExecutionOptions` with length validation and persisted reuse❌ NO - not documented as first-class execution metadata❌ NO - app-defined⚠️ PARTIAL - workflow/activity IDs and idempotency patterns⚠️ PARTIAL - execution names/tokens, service-specific semanticsSuccessful-step output aggregation✅ YES - persisted successful outputs rehydrate idempotent run reuse⚠️ PARTIAL - workflow/activity outputs exist, but this package contract is not documented❌ NO - app-defined✅ YES - workflow history/result model⚠️ PARTIAL - state input/output paths, not Laravel step result objectsTransaction-scoped transition writes⚠️ PARTIAL - step transitions use repository transactions and atomic upserts; compensation audit/finalization can be separate writes⚠️ PARTIAL - package/app persistence model⚠️ PARTIAL - marking store/app transaction concern✅ YES - managed event-history durability✅ YES - managed execution-history durabilityRuntime-abort recovery before surfacing infrastructure failures✅ YES - best-effort failure state plus compensation before rethrow⚠️ PARTIAL - retries/error handling, recovery policy is workflow-defined⚠️ PARTIAL - app-defined✅ YES - durable execution/retry recovery⚠️ PARTIAL - `Retry`, `Catch`, and redrive behaviorRetention pruning for persisted telemetry✅ YES - `flow:prune` keeps pending/running rows intact❌ NO - not documented as built-in❌ NO - app-defined⚠️ PARTIAL - service retention configuration, not package command⚠️ PARTIAL - managed history/log retention, not app commandBusiness-impact projection on every result✅ YES - `businessImpact` is part of every `FlowStepResult`❌ NO - not documented❌ NO - not a workflow component concern❌ NO - app-defined❌ NO - app-definedQueue-backed workers today⚠️ PARTIAL - `Flow::dispatch()` queues an after-commit `RunFlowJob` with per-dispatch locking, configurable lock-held release delay, completed-duplicate no-op handling, guarded Laravel-native tries/backoff metadata, and sync/database queue coverage✅ YES - Laravel queue/worker support❌ NO - not native✅ YES - worker-based execution✅ YES - managed orchestrationReplay/redrive of failed executions today⚠️ PARTIAL - `flow:replay {runId}` creates a new linked run from persisted terminal input and warns on definition drift; deterministic event-history replay is not shipped⚠️ PARTIAL - durable long-running workflow model; exact replay semantics differ❌ NO - not native✅ YES - deterministic replay/event history✅ YES - Standard Workflow redriveLow setup friction✅ YES - `composer require` plus optional config/migration publish⚠️ PARTIAL - Laravel queues/workers and optional Waterline UI✅ YES - Composer package and framework config❌ NO - service/cluster plus workers❌ NO - AWS account, IAM, state-machine definitionsSelf-hosted with no external workflow service✅ YES - runs inside the Laravel app; DB optional✅ YES - Laravel app/queue infrastructure✅ YES - application component❌ NO - requires Temporal service/cluster❌ NO - AWS-managed serviceOpen-source package/license✅ YES - Apache-2.0✅ YES - MIT✅ YES - MIT✅ YES - MIT core/server and SDKs❌ NO - proprietary managed serviceCompetitor snapshot checked against Durable Workflow, Symfony Workflow, Temporal, and AWS Step Functions documentation on 2026-05-05. `laravel-flow` is **deliberately positioned** as the lightest Laravel-native dependency in the table. If you already run Temporal or AWS Step Functions and need their queue/replay/redrive guarantees today, use them. If you want saga semantics, dry-run, business-impact projection, opt-in persistence, signed webhook delivery, headless dashboard contracts, and a companion web admin UI inside an existing Laravel app, this is the package.

---

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

[](#installation)

```
composer require padosoft/laravel-flow
```

Publish the config (optional — the engine works with defaults):

```
php artisan vendor:publish --tag=laravel-flow-config
```

Publish the v0.2 persistence migrations only when you are opting into DB-backed storage:

```
php artisan vendor:publish --tag=laravel-flow-migrations
php artisan migrate
```

The in-memory engine path still works without migrations. To persist runtime runs, enable `LARAVEL_FLOW_PERSISTENCE_ENABLED=true`; dry-runs remain simulation-only and do not write to the database.

Prune old terminal persistence records with the built-in retention command:

```
php artisan flow:prune --days=90 --dry-run
php artisan flow:prune --days=90
```

`flow:prune` deletes only terminal runs (`succeeded`, `failed`, `compensated`, `aborted`) with `finished_at` older than the cutoff. Matching `flow_steps` and `flow_audit` rows are deleted in the same batch transaction; running and pending rows are left untouched. Use `LARAVEL_FLOW_RETENTION_DAYS=90` to make `--days` optional, and `--force` for non-interactive production runs.

Replay a terminal persisted run as a new linked run:

```
php artisan flow:replay 00000000-0000-4000-8000-000000000001
```

`flow:replay` reads the original persisted input and current registered definition, then creates a fresh persisted run with `replayed_from_run_id` pointing at the original. It does not mutate the original run and refuses to replay pending/running rows. If the stored step names/handlers differ from the current definition, the command warns and still uses the current definition. Redacted persisted input stays redacted during replay.

> **Requirements**
>
> - PHP 8.3+
> - Laravel 13.x

---

Quick start
-----------

[](#quick-start)

```
use Padosoft\LaravelFlow\Facades\Flow;
use Padosoft\LaravelFlow\FlowContext;
use Padosoft\LaravelFlow\FlowStepHandler;
use Padosoft\LaravelFlow\FlowStepResult;

// 1. Define a handler.
class ValidatePromotionInput implements FlowStepHandler
{
    public function execute(FlowContext $context): FlowStepResult
    {
        if ($context->input['discount_pct'] > 90) {
            return FlowStepResult::failed(new \DomainException('Discount > 90%'));
        }

        return FlowStepResult::success(['validated_at' => now()->toIso8601String()]);
    }
}

// 2. Register a flow.
Flow::define('promotion.create')
    ->withInput(['brand', 'discount_pct'])
    ->step('validate', ValidatePromotionInput::class)
    ->register();

// 3. Execute.
$run = Flow::execute('promotion.create', ['brand' => 'acme', 'discount_pct' => 25]);

if ($run->status === \Padosoft\LaravelFlow\FlowRun::STATUS_SUCCEEDED) {
    // Done.
} else {
    // $run->failedStep tells you which step blew up.
    // $run->compensated tells you whether rollback ran.
}
```

---

Usage examples
--------------

[](#usage-examples)

### Correlation and idempotency

[](#correlation-and-idempotency)

```
use Padosoft\LaravelFlow\FlowExecutionOptions;

$run = Flow::execute(
    'promotion.create',
    $input,
    FlowExecutionOptions::make(
        correlationId: 'checkout-2026-0001',
        idempotencyKey: 'tenant-42:promotion-abc',
    ),
);
```

When persistence is enabled, `correlationId` and `idempotencyKey` are stored on `flow_runs`. Both values are trimmed, empty strings become `null`, and non-empty values are limited to 255 characters to match the published migrations. A later persisted execution with the same idempotency key returns the existing run state without executing handlers again. Dry-runs still avoid persistence writes.

### Queue dispatch foundation

[](#queue-dispatch-foundation)

```
use Padosoft\LaravelFlow\FlowExecutionOptions;

Flow::dispatch(
    'promotion.create',
    $input,
    FlowExecutionOptions::make(
        correlationId: 'checkout-2026-0001',
        idempotencyKey: 'tenant-42:promotion-abc',
    ),
);
```

`Flow::dispatch()` validates the registered definition and required input before queuing `RunFlowJob`. The job dispatches after the current database transaction commits and takes a per-dispatch cache lock before execution; duplicate deliveries that find the lock held are released after the smaller of `queue.lock_retry_seconds` and `queue.lock_seconds`, while duplicates that arrive after the dispatch completed are acknowledged as no-ops. Configure `queue.tries` and `queue.backoff_seconds` to stamp Laravel-native retry metadata into the queued job payload for workers and Horizon. Because async Laravel workers retry the whole `RunFlowJob` from the beginning, policies that can re-run a flow are rejected until step-level retry semantics are available. The `sync` queue driver ignores worker retry metadata and remains allowed for local/test dispatches. The worker resolves the current `FlowEngine` and executes the same definition with the serialized input and execution options. Compensation still defaults to reverse-order rollback; set `compensation_strategy=parallel` only for flows whose compensators are independent and idempotent.

### Approval gates

[](#approval-gates)

```
Flow::define('promotion.publish')
    ->step('prepare', PreparePromotion::class)
        ->compensateWith(UndoPreparedPromotion::class)
    ->approvalGate('manager')
    ->step('publish', PublishPromotion::class)
    ->register();

// Enable laravel-flow.persistence.enabled before executing so a persisted approval token is issued.
$pausedRun = Flow::execute('promotion.publish', $input);
$token = $pausedRun->approvalTokens['manager']->plainTextToken;

$resumedRun = Flow::resume($token, ['decision' => 'approved'], ['user_id' => 123]);
// To reject instead of approving that pending token:
// $rejectedRun = Flow::reject($token, ['reason' => 'duplicate'], ['user_id' => 123]);
```

Approval resume/reject requires persistence and a shared cache store that supports Laravel atomic locks. Custom persistence backends must bind their approval backend to `ApprovalRepository`, implement `ApprovalDecisionRepository` on that approval backend for decided-token lookup, run-status-conditional consumes, and one-time pending-token reissue for downstream approval gates, return a `ConditionalRunRepository` from `FlowStore::runs()` for paused-run claims, keep the approval backend and `FlowStore::runs()` on the same durable storage boundary so conditional consumes and paused-run claims observe the same run state, and apply the package payload redactor consistently to approval decision JSON. Backends that delegate approval JSON redaction to laravel-flow can implement `RedactorAwareApprovalRepository` so `Flow::resume()` / `Flow::reject()` inject the same execution-frozen redactor used for step/run writes. The process-local `array` cache store is rejected for approval decisions because concurrent HTTP/API requests would not share the lock. The decision lock is keyed by run, so retries of older gate tokens serialize with a later gate resume on the same run. The plain token is returned only on the immediate paused run, while `flow_approvals` stores the current token hash, at most one previous hash for a reissued downstream-gate token, expiry, redacted decision payload, and redacted actor metadata. Custom approval repositories that support downstream token reissue must resolve both current and previous hashes in lookup, consume, and expiry paths so both handed-out tokens remain valid until expiry or decision. If a retry of an older approved token finds the run paused at a later approval gate, Laravel Flow can rotate that later gate's pending token hash once while it is still unexpired, keep the previous hash valid, refresh the paused-step expiry metadata, and return a fresh plain token on the current run. If the per-run lock is already held, old-token retries ask the caller to retry instead of returning a tokenless paused run or reissuing without owning the lock. `Flow::resume()` marks the approval gate succeeded and continues from the next unfinished step without rerunning prior or already-persisted downstream successes. If a duplicate resume sees that the run is already `running`, it returns the current persisted state instead of re-entering downstream handlers while another process may still own the side effect. Later steps receive the stored approval payload and stored approval actor metadata, so configured secret keys are redacted before handler context is reconstructed. `Flow::reject()` records the gate as failed and runs compensators for prior completed steps. Resume reconstructs context from persisted input and step outputs, so values redacted before storage remain redacted after resume.

### Signed webhook outbox delivery

[](#signed-webhook-outbox-delivery)

```
php artisan flow:deliver-webhooks

// Tune throughput and pacing for transient errors.
php artisan flow:deliver-webhooks --batch=10 --sleep-ms=250
```

`flow:deliver-webhooks` processes pending rows from `flow_webhook_outbox` and sends signed HTTP POST payloads to `laravel-flow.webhook.url`. Each call can be retried with exponential backoff and a configurable attempt cap. Rows are marked `delivered`, `pending` (with a future `available_at`), or `failed`.

The command requires `laravel-flow.webhook.enabled=true` and a non-empty valid `laravel-flow.webhook.url`.

### Compensation chain (saga rollback)

[](#compensation-chain-saga-rollback)

```
class PersistPromotion implements FlowStepHandler { /* writes a DB row */ }
class ReversePromotion implements FlowCompensator  { /* deletes the row */ }

Flow::define('promotion.create')
    ->withInput(['brand', 'discount_pct'])
    ->step('validate', ValidatePromotionInput::class)
    ->step('persist', PersistPromotion::class)
        ->compensateWith(ReversePromotion::class)
    ->step('publish', PublishPromotionToCDN::class)  // imagine this fails
    ->register();

$run = Flow::execute('promotion.create', $input);

// If 'publish' fails:
//   $run->status      === FlowRun::STATUS_FAILED
//   $run->failedStep  === 'publish'
//   $run->compensated === true   // ReversePromotion ran
```

### Dry-run / impact projection

[](#dry-run--impact-projection)

```
class SimulatePromotionImpact implements FlowStepHandler
{
    public function execute(FlowContext $context): FlowStepResult
    {
        $impact = [
            'expected_users_reached' => 12_400,
            'projected_revenue_eur'  => 18_900.00,
        ];

        return FlowStepResult::success(output: [], businessImpact: $impact);
    }
}

Flow::define('promotion.create')
    ->step('simulate', SimulatePromotionImpact::class)
        ->withDryRun(true)
    ->step('persist', PersistPromotion::class)        // skipped in dry mode
    ->register();

$dryRun = Flow::dryRun('promotion.create', $input);

$businessImpact = $dryRun->stepResults['simulate']->businessImpact;
// ['expected_users_reached' => 12400, 'projected_revenue_eur' => 18900.00]
```

### Subscribing to the audit trail

[](#subscribing-to-the-audit-trail)

```
use Illuminate\Support\Facades\Event;
use Padosoft\LaravelFlow\Events\FlowStepStarted;
use Padosoft\LaravelFlow\Events\FlowStepCompleted;
use Padosoft\LaravelFlow\Events\FlowStepFailed;
use Padosoft\LaravelFlow\Events\FlowCompensated;

Event::listen(function (FlowStepStarted $e) {
    logger()->info('flow.step.started', [
        'flow_run_id'     => $e->flowRunId,
        'definition_name' => $e->definitionName,
        'step_name'       => $e->stepName,
        'dry_run'         => $e->dryRun,
    ]);
});

Event::listen(function (FlowStepFailed $e) {
    // Wire to Sentry / Datadog / your audit table.
});
```

---

Configuration reference
-----------------------

[](#configuration-reference)

```
// config/laravel-flow.php
$queueLockSeconds = env('LARAVEL_FLOW_QUEUE_LOCK_SECONDS', 3600);
$queueLockRetrySeconds = env('LARAVEL_FLOW_QUEUE_LOCK_RETRY_SECONDS', 30);
$queueTries = env('LARAVEL_FLOW_QUEUE_TRIES', null);
$queueBackoffSeconds = env('LARAVEL_FLOW_QUEUE_BACKOFF_SECONDS', null);
$approvalTokenTtlMinutes = env('LARAVEL_FLOW_APPROVAL_TOKEN_TTL_MINUTES', 1440);
$webhookTimeoutSeconds = env('LARAVEL_FLOW_WEBHOOK_TIMEOUT_SECONDS', 5);
$webhookRetryBaseDelaySeconds = env('LARAVEL_FLOW_WEBHOOK_RETRY_BASE_DELAY_SECONDS', 30);
$webhookMaxAttempts = env('LARAVEL_FLOW_WEBHOOK_MAX_ATTEMPTS', 3);

return [
    'default_storage'        => env('LARAVEL_FLOW_STORAGE', null),
    'persistence'            => [
        'enabled'   => env('LARAVEL_FLOW_PERSISTENCE_ENABLED', false),
        'redaction' => [
            'enabled'     => env('LARAVEL_FLOW_REDACTION_ENABLED', true),
            'replacement' => env('LARAVEL_FLOW_REDACTION_REPLACEMENT', '[redacted]'),
            'keys'        => ['api_key', 'authorization', 'password', 'secret', 'token'],
        ],
        'retention' => [
            'days' => env('LARAVEL_FLOW_RETENTION_DAYS', null),
        ],
    ],
    'queue'                  => [
        'lock_store'   => env('LARAVEL_FLOW_QUEUE_LOCK_STORE', null),
        'lock_seconds' => is_numeric($queueLockSeconds) && (int) $queueLockSeconds >= 1
            ? (int) $queueLockSeconds
            : 3600,
        'lock_retry_seconds' => is_numeric($queueLockRetrySeconds) && (int) $queueLockRetrySeconds >= 1
            ? (int) $queueLockRetrySeconds
            : 30,
        'tries' => $queueTries,
        'backoff_seconds' => $queueBackoffSeconds, // set LARAVEL_FLOW_QUEUE_BACKOFF_SECONDS=5,30,120 for a retry schedule
    ],
    'approval'               => [
        'token_ttl_minutes' => is_numeric($approvalTokenTtlMinutes) && (int) $approvalTokenTtlMinutes >= 1
            ? (int) $approvalTokenTtlMinutes
            : 1440,
    ],
    'webhook'               => [
        'enabled' => env('LARAVEL_FLOW_WEBHOOK_ENABLED', false),
        'url' => env('LARAVEL_FLOW_WEBHOOK_URL', ''),
        'secret' => env('LARAVEL_FLOW_WEBHOOK_SECRET', null),
        'retry_base_delay_seconds' => is_numeric($webhookRetryBaseDelaySeconds) && (int) $webhookRetryBaseDelaySeconds > 0
            ? (int) $webhookRetryBaseDelaySeconds
            : 30,
        'max_attempts' => is_numeric($webhookMaxAttempts) && (int) $webhookMaxAttempts > 0
            ? (int) $webhookMaxAttempts
            : 3,
        'timeout_seconds' => is_numeric($webhookTimeoutSeconds) && (int) $webhookTimeoutSeconds >= 1
            ? (int) $webhookTimeoutSeconds
            : 5,
    ],
    'audit_trail_enabled'    => env('LARAVEL_FLOW_AUDIT_ENABLED', true), // events; DB audit rows require this=true, persistence.enabled=true, and a non-dry-run execution
    'dry_run_default'        => env('LARAVEL_FLOW_DRY_RUN_DEFAULT', false),
    'step_timeout_seconds'   => (int) env('LARAVEL_FLOW_STEP_TIMEOUT', 300), // v0.2
    'compensation_strategy'  => env('LARAVEL_FLOW_COMPENSATION', 'reverse-order'), // reverse-order|parallel
    'compensation_parallel_driver' => env('LARAVEL_FLOW_COMPENSATION_PARALLEL_DRIVER', 'process'),
];
```

KeyDefaultEffect`default_storage``null`DB connection used by persistence repositories. Inherits app default when `null`.`persistence.enabled``false`Enables synchronous engine writes to `flow_runs` and `flow_steps`; `flow_audit` writes also require `audit_trail_enabled=true` and a non-dry-run execution. Dry-runs do not write.`persistence.redaction`common secretsRedacts configured JSON payload keys before run, step, and audit payloads are stored.`persistence.retention.days``null`Default retention window for `php artisan flow:prune`; pass `--days` to override per run.`queue.lock_store``null`Cache store used for queued run locks and approval resume/reject locks. When `null`, `Flow::dispatch()` captures the current `cache.default` store into the job; queued workers require a shared atomic lock store, while process-local `array` is accepted only with the `sync` queue driver. Approval resume/reject always rejects `array`.`queue.lock_seconds``3600`TTL for the per-dispatch queue lock used by `RunFlowJob` and the per-run approval decision lock used by `Flow::resume()` / `Flow::reject()`; set it longer than the expected maximum queued run or approval resume/reject runtime because Laravel's portable lock contract cannot renew it.`queue.lock_retry_seconds``30`Delay before a duplicate delivery retries when the per-dispatch lock is still held; the job caps it at `queue.lock_seconds`.`queue.tries``null`Optional Laravel job attempts value stamped onto `RunFlowJob`; `null` leaves the worker/connection default in control, while `0` preserves Laravel's unlimited-retry semantics. Async values that can retry the whole run are rejected until step-level retry or replay semantics are available.`queue.backoff_seconds``null`Optional Laravel job backoff value stamped onto `RunFlowJob`; use a single integer or comma-separated/list values such as `5,30,120`. Async backoff schedules that can retry the whole run are rejected until step-level retry or replay semantics are available.`approval.token_ttl_minutes``1440`Expiry window for `ApprovalTokenManager` one-time tokens consumed by `Flow::resume()` / `Flow::reject()`. Only token hashes are stored in `flow_approvals`; the plain token is returned once from issuance.`webhook.enabled``false`Enables signed lifecycle delivery. Set `true` only when your app can reach the webhook URL from the command schedule or worker.`webhook.url``''`URL endpoint receiving lifecycle event JSON from `flow:deliver-webhooks`; the command rejects empty or syntactically invalid URL values (HTTPS strongly recommended for production).`webhook.secret``null`Shared HMAC secret for signing `X-Laravel-Flow-Signature: t=...,v1=...` headers. Leave empty to disable signing.`webhook.retry_base_delay_seconds``30`Base delay (seconds) for exponential backoff between outbox retries.`webhook.max_attempts``3`Maximum delivery attempts before `flow_webhook_outbox.status` becomes `failed`.`webhook.timeout_seconds``5`Per-request delivery timeout in seconds (must be &gt;=1).`audit_trail_enabled``true`When `false`, suppresses every `FlowStep*` / `FlowCompensated` event and persisted audit row; persisted audit rows also require persistence and a non-dry-run execution.`dry_run_default``false`When `true`, `Flow::execute()` behaves like `dryRun()` — guard rail for staging environments.`step_timeout_seconds``300`Reserved for follow-up queued step execution; the current `RunFlowJob` dispatch slice does not enforce per-step timeouts.`compensation_strategy``reverse-order`Supported values are `reverse-order` and `parallel`; use `parallel` only when completed compensators are independent, idempotent, and safe without reverse-order dependencies.`compensation_parallel_driver``process`Laravel Concurrency driver used only for `compensation_strategy=parallel` and only when the engine uses the global Laravel container; set `sync` for deterministic local/tests or `process`/`fork` for actual parallelism when your app supports it. If the driver cannot be resolved or run, the engine falls back to injected-container in-process compensation.When persistence is enabled, synchronous `FlowStep*` listener or persistence failures are rethrown after the engine records best-effort recovery state and compensates completed steps. `FlowCompensated` listener failures are swallowed after the compensation audit row is durable so rollback is not interrupted. Wrap `Flow::execute()` in application-level exception handling anywhere infrastructure outages must be surfaced separately from business step failures.

Custom `FlowStore` implementations that need the same per-execution `PayloadRedactor` used by engine error-text sanitization should implement `Padosoft\LaravelFlow\Contracts\RedactorAwareFlowStore`. The engine calls `withPayloadRedactor()` once per persisted execution before writing run, step, and audit telemetry; implementations that keep transaction state on the store should return the same instance or a state-sharing decorator. `PayloadRedactor` decorators that wrap the package execution-scoped redactor should implement `Padosoft\LaravelFlow\Contracts\CurrentPayloadRedactorProvider` so multi-field repository writes can reuse one stable inner redactor without changing each JSON payload shape.

---

Architecture
------------

[](#architecture)

```
┌────────────────────┐     define / register      ┌─────────────────────┐
│ FlowDefinitionBuilder ──────────────────────────► FlowDefinition       │
└──────────┬─────────┘                             │  - name             │
           │                                       │  - requiredInputs   │
           │                                       │  - list   │
           │                                       └──────────┬──────────┘
           │                                                  │
           │  Flow::execute / Flow::dryRun                    ▼
           ▼                                       ┌─────────────────────┐
   ┌──────────────────┐  iterate steps             │ FlowEngine          │
   │   FlowEngine     │ ───────────────────────────►   - definitions[]   │
   │  ::execute()     │                            │   - container DI    │
   └──────┬───────────┘                            │   - event dispatch  │
          │                                        └─────────┬───────────┘
          │ container->make(handlerFqcn)                     │
          ▼                                                  │
   ┌──────────────────┐                                      │
   │ FlowStepHandler  │ ──► FlowStepResult ─────────────────►│
   └──────────────────┘                                      │
          │ failure                                          │
          ▼                                                  │
   ┌──────────────────┐                                      │
   │ Compensation     │ walks backwards                      │
   │ ::reverse-order  │ ──► FlowCompensator::compensate()    │
   └──────────────────┘                                      │
                                                             ▼
                                                   ┌─────────────────────┐
                                                   │ FlowRun             │
                                                   │  - id (uuid v4)     │
                                                   │  - status           │
                                                   │  - failedStep       │
                                                   │  - compensated      │
                                                   │  - stepResults{}    │
                                                   │  - startedAt/finishedAt
                                                   └─────────────────────┘

```

Every box is one PHP class under `src/`. The engine path is still synchronous and in-memory by default; when persistence is enabled, runtime runs and steps are written to `flow_runs` and `flow_steps` for non-dry-run executions. Audit transitions are written to `flow_audit` only for non-dry-run executions while persistence and `audit_trail_enabled` are both enabled. Dry-runs never write audit rows. `Flow::dispatch()` queues an after-commit `RunFlowJob` with per-dispatch locking, database queue coverage, and guarded Laravel retry/backoff metadata. `flow:replay` creates new linked runs from terminal persisted input, and `compensation_strategy=parallel` batches independent compensators through Laravel Concurrency.

### Public API surface (v1.0)

[](#public-api-surface-v10)

The package marks every class with `@api` (stable, SemVer-covered) or `@internal` (implementation detail, not covered by SemVer). Public surface:

- **Facade**: `Padosoft\LaravelFlow\Facades\Flow`.
- **Engine + DTOs**: `FlowEngine`, `FlowDefinitionBuilder`, `FlowDefinition`, `FlowStep`, `FlowExecutionOptions`, `FlowRun`, `FlowStepResult`, `FlowContext`, `IssuedApprovalToken`, `ApprovalGate`, `ApprovalTokenManager`, `WebhookDeliveryClient`, `WebhookDeliveryResult`.
- **Hooks**: `FlowStepHandler`, `FlowCompensator` interfaces.
- **Events**: every class under `Padosoft\LaravelFlow\Events\*`.
- **Exceptions**: every class under `Padosoft\LaravelFlow\Exceptions\*`.
- **Extension contracts**: every interface under `Padosoft\LaravelFlow\Contracts\*` (custom `FlowStore`, `RunRepository`, `StepRunRepository`, `AuditRepository`, `ApprovalRepository`, `ApprovalDecisionRepository`, `ConditionalRunRepository`, `PayloadRedactor`, `CurrentPayloadRedactorProvider`, `RedactorAwareFlowStore`, `RedactorAwareApprovalRepository` implementations).
- **Dashboard contracts**: `FlowDashboardReadModel`, the read DTOs in `Padosoft\LaravelFlow\Dashboard\*`, and `DashboardActionAuthorizer` + the `DenyAllAuthorizer` / `AllowAllAuthorizer` bindings.

The `tests/Contract/PublicApiContractTest` testsuite pins the v1.0 surface so a follow-up patch cannot silently rename or remove an `@api` class, method, or constant. Internal namespaces (`Persistence`, `Models`, `Queue`, `Jobs`, `Console`) may change in any minor release; route consumers through the public contracts instead. See [`docs/UPGRADE.md`](docs/UPGRADE.md) for the full SemVer policy and upgrade chain.

### Companion dashboard

[](#companion-dashboard)

The package itself is headless — there is no embedded UI. The companion app `padosoft/padosoft-laravel-flow-dashboard` (separate repo) consumes the dashboard contracts via Composer path repository during development and from Packagist in production. The complete brief for an AI agent or human team building that app is at [`docs/DASHBOARD_APP_SPEC.md`](docs/DASHBOARD_APP_SPEC.md).

---

AI vibe-coding pack
-------------------

[](#ai-vibe-coding-pack)

🚀 **Every Padosoft package ships with the same vibe-coding pack** — drop the `.claude/` directory into Claude Code or GitHub Copilot and you get:

- **Skills** under `.claude/skills/` — reviewer-validated playbooks for `copilot-pr-review-loop`, `pre-push-self-review`, `test-count-readme-sync`, and more.
- **Rules** under `.claude/rules/` — coding standards (type hints, early return, no debug in commits, code structure, naming conventions, PR workflow). For `laravel-flow`, repo-local rules define the Laravel 13 package baseline and PR workflow.
- **Agents** under `.claude/agents/` — pre-wired sub-agent definitions (`admin-interface-architect`, `playwright-enterprise-tester`).
- **Commands** under `.claude/commands/` — slash-command templates (`/create-job`, `/domain-scaffold`, `/playwright-tester`, `/pagespeed-review`).
- **Instructions** under `.claude/instructions/` — runtime safety guardrails (`testing-safety.md`).

The pack is the same baseline used across all `padosoft/*` repos. It is opt-in: delete `.claude/` if you don't use Claude Code or Copilot — nothing else depends on it.

---

Testing — Default + Live
------------------------

[](#testing--default--live)

The default `phpunit` invocation runs only the offline `Unit` + `Architecture` testsuites and never makes a network call:

```
composer install
composer validate --strict --no-check-publish
composer format:test
composer analyse
composer test
```

The `Live` testsuite is **opt-in** and reserved for v0.2+ scenarios that need a real external dependency (queue worker, webhook receiver). Every Live test self-skips unless `LARAVEL_FLOW_LIVE=1` is set:

```
LARAVEL_FLOW_LIVE=1 vendor/bin/phpunit --testsuite Live
```

CI runs Pint (style), PHPStan (level 6), and the Unit + Architecture suites through Composer scripts on the PHP 8.3 / 8.4 / 8.5 × Laravel 13 matrix for pushes to `main` and PRs targeting `main` or `task/**`.

---

Roadmap
-------

[](#roadmap)

VersionScopeTargetv0.1In-memory engine, fluent builder, dry-run, reverse-order compensation, four audit event classes, business-impact field on results, Facade. Architecture test enforces standalone-agnostic.code completev0.2Persistence core: `flow_runs` / `flow_steps` / `flow_audit` tables, synchronous engine writes, redacted payload storage, correlation/idempotency keys, terminal-run retention pruning, queued dispatch foundation with per-dispatch locking, database queue coverage, guarded Laravel-native retry/backoff metadata, `flow:replay` for terminal persisted runs, and opt-in parallel compensation for independent compensators.Q3 2026v0.3Approval-gate primitive, hashed one-time approval-token issuance, persisted API flow resume/reject controls, CLI approval commands, and signed webhook outbox delivery now landed (`flow:approve`, `flow:reject`, `flow:deliver-webhooks` included).Q4 2026v1.0Package-side dashboard contracts (`FlowDashboardReadModel`, `DashboardActionAuthorizer`, `DenyAllAuthorizer` default), stable API marking, semver guarantee, migration helpers from Durable Workflow / Symfony Workflow, and companion app spec at `docs/DASHBOARD_APP_SPEC.md`.2027---

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

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md). Community PRs target `main`; enterprise roadmap work uses the macro/subtask PR loop documented in `AGENTS.md`.

---

Security
--------

[](#security)

Report vulnerabilities privately to `lorenzo.padovani@padosoft.com` per [SECURITY.md](SECURITY.md). Operational guarantees the package enforces:

- **Approval tokens are SHA-256 hashed at rest.** The plain token is returned only on the immediate `FlowRun` at issuance time and is never recoverable from `flow_approvals`. The dashboard contract takes a token hash, not the plain value, so authorization can run without the secret.
- **JSON payload redaction.** `flow_runs.input/output/business_impact`, `flow_steps.input/output/business_impact`, `flow_audit.payload`, and `flow_approvals.payload/actor` pass through the configured `PayloadRedactor` before storage. Default keys cover common secret-looking fields; the policy is configurable through `laravel-flow.persistence.redaction`. The dashboard read DTOs (`RunDetail`, `ApprovalSummary`) return whatever is stored — host apps that disable `persistence.redaction.enabled` MUST add their own redaction layer before rendering.
- **Webhook payload signing.** When `webhook.secret` is set, every outbox delivery carries `X-Laravel-Flow-Signature: t=,v1=` so receivers can verify authenticity. Empty secret disables signing rather than emitting a bogus header.
- **Dashboard authorization is deny-by-default.** `DashboardActionAuthorizer` is bound to `DenyAllAuthorizer`. Production deployments MUST replace the binding with their own RBAC implementation. `AllowAllAuthorizer` is shipped explicitly for development and never registered automatically.
- **Append-only audit at runtime.** `flow_audit` rows cannot be updated or deleted via Eloquent `save()`/`delete()`. Bulk `update()` / `delete()` / `forceDelete()` are blocked by a custom Eloquent builder. The `flow:prune` retention command is the only supported deletion path and is documented as such.
- **Run-level locking.** `Flow::resume()` and `Flow::reject()` claim a per-run shared cache lock to serialize approval decisions. The process-local `array` cache store is rejected because it cannot synchronize across HTTP / queue workers.
- **No secrets in logs or error messages.** Engine text-redaction normalises common secret keys across snake\_case / kebab-case / camelCase before persisting exception messages.

Enterprise notes
----------------

[](#enterprise-notes)

The package is designed to live inside a Laravel application's process and database, not in a separate workflow service. That keeps the operational footprint small (no dedicated workflow cluster) at the cost of cross-language workflows and managed multi-region failover. If you need either, evaluate a managed durable-workflow runtime instead — see [`docs/MIGRATION_DURABLE.md`](docs/MIGRATION_DURABLE.md) for the trade-off table.

For teams adopting laravel-flow:

- Use macro branches and subtask PRs documented in `AGENTS.md`. Every PR requires GitHub Copilot Code Review plus the local Composer-script gates (`composer validate --strict --no-check-publish`, `composer format:test`, `composer analyse`, `composer test`).
- Bind a custom `DashboardActionAuthorizer` before exposing the dashboard read model to operators. The default `DenyAllAuthorizer` is intentional.
- Schedule `flow:prune` to bound `flow_audit` and `flow_webhook_outbox` growth. Retention is the only supported audit-deletion path.
- Schedule `flow:deliver-webhooks` (e.g. once a minute) when webhook delivery is enabled. The command is idempotent and uses lease-based row claims with a compare-and-set guard, so multiple concurrent workers are safe.
- Configure `queue.lock_store` to a shared atomic cache (`redis`, `memcached`, `database`, `dynamodb`) before enabling queued dispatch with multiple workers. The `array` store is accepted only on the `sync` queue driver and is rejected for approval decisions.
- Read [`docs/UPGRADE.md`](docs/UPGRADE.md) before bumping major versions; v1.0 marked the public-vs-internal contract and SemVer policy.

---

License
-------

[](#license)

[Apache-2.0](LICENSE).

Built by [Padosoft](https://padosoft.com) — part of the [v4.0 ecosystem](https://github.com/lopadova/AskMyDocs).

###  Health Score

51

—

FairBetter than 95% of packages

Maintenance93

Actively maintained with recent releases

Popularity29

Limited adoption so far

Community14

Small or concentrated contributor base

Maturity57

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 99.5% 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.

###  Release Activity

Cadence

Every ~16 days

Total

4

Last Release

34d ago

Major Versions

v0.1.0 → v1.0.02026-05-05

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/10467699?v=4)[Lorenzo](/maintainers/lopadova)[@lopadova](https://github.com/lopadova)

---

Top Contributors

[![lopadova](https://avatars.githubusercontent.com/u/10467699?v=4)](https://github.com/lopadova "lopadova (206 commits)")[![ImgBotApp](https://avatars.githubusercontent.com/u/31427850?v=4)](https://github.com/ImgBotApp "ImgBotApp (1 commits)")

---

Tags

compensationdistributed-systemsidempotencylaravelorchestrationphpsagastate-machineworkflowlaravelworkflowaudit-trailorchestrationsagacompensationbusiness-impactdry-run

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/padosoft-laravel-flow/health.svg)

```
[![Health](https://phpackages.com/badges/padosoft-laravel-flow/health.svg)](https://phpackages.com/packages/padosoft-laravel-flow)
```

###  Alternatives

[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M133](/packages/roots-acorn)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[flarum/core

Delightfully simple forum software.

211.4M2.4k](/packages/flarum-core)[propaganistas/laravel-disposable-email

Disposable email validator

6023.0M7](/packages/propaganistas-laravel-disposable-email)

PHPackages © 2026

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