Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Flows Laravel Package

juststeveking/laravel-flows

Laravel Flows offers a simple, expressive way to define and run multi-step workflows in Laravel. Compose reusable steps, control execution order, and keep complex business processes testable, readable, and maintainable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin immediately.

composer require juststeveking/laravel-flows:1.0.4

Pin the exact version rather than using a caret range. 1.0.4 shipped transaction handling, step validation, and generics changes as a patch release — treat every upstream bump as behavior-affecting and read the diff before upgrading. There are no migrations and (very likely) no required config; if the package registers a publishable config or migration, publish it once during the pilot and commit it, so config drift is visible in code review.

Look at the generated class first, not the README. Run the scaffold command (check php artisan list | grep -i flow for the exact signature — typically make:flow) against a throwaway name:

php artisan make:flow CheckoutFlow

The generated class is the contract. It shows you where steps are declared (usually a steps() array of invokable class names, or a fluent ->step() chain), where validation is declared, and what the entry point is called. Read it before writing anything else; nearly all day-to-day confusion in this package comes from guessing the contract instead of reading the scaffold.

Your first use case should be boring and observable. Pick a three-step process you already have scattered across a controller and a service class — user onboarding, or the happy path of a checkout — where each step is a discrete write you can point at. Do not start with your most complex workflow; you want the first flow to validate the pattern, not stress it.

Sketch the shape:

// app/Flows/OnboardingFlow.php  (generated, then edited)
class OnboardingFlow extends Flow
{
    public function steps(): array
    {
        return [
            CreateAccount::class,
            ProvisionWorkspace::class,
            SendWelcomeEmail::class,
        ];
    }
}
// app/Data/OnboardingPayload.php
final readonly class OnboardingPayload
{
    public function __construct(
        public string $email,
        public string $name,
        public ?int $workspaceId = null,
    ) {}
}

Then invoke it from a controller or job:

$flow = app(OnboardingFlow::class);   // container-resolved: steps get DI for free
$result = $flow->run(new OnboardingPayload(...));

Confirm the entry-point method name from the scaffold — run, handle, or __invoke. Write one passing feature test through the controller before you add a second flow. That test is your characterization baseline for the next upgrade.

Implementation Patterns

Typed payload DTOs, one per flow. Use a final readonly class as the payload and pass it through unchanged. This gives you static analysis, IDE completion inside every step, and a single place to see the flow's total data contract. Avoid array<string, mixed> payloads — they erase the one advantage this package has over a hand-rolled pipeline.

Every step returns the payload. The convention that matters most:

final class ProvisionWorkspace
{
    public function __invoke(OnboardingPayload $payload): OnboardingPayload
    {
        $workspace = Workspace::create([...]);

        return $payload->withWorkspace($workspace->id);
    }
}

Because the payload is readonly, steps return new instances. That's a feature — it makes the transition between steps explicit and diffable — but it also means a forgotten return silently hands null (or the previous payload) forward. Enforce it in review and with a return type declaration on every __invoke.

Steps are thin, container-resolved, and single-purpose. Each step is an invokable class with dependencies type-hinted in the constructor. A step that's more than ~30 lines is usually two steps, or a step that's secretly a service class the flow shouldn't own. Steps that are genuinely reusable across flows (a SendNotification, an AttachAuditLog) should be written generically against an interface and shared.

Transactions at the flow boundary, never per step. If a flow mutates order + payment + inventory, wrap the whole run, not each step:

DB::transaction(fn () => app(CheckoutFlow::class)->run($payload));

Per-step transactions give you a partially committed process that looks successful — the worst possible failure mode. Pick exactly one transaction layer (the flow boundary, or the package's built-in transaction support if 1.0.4's semantics fit your needs; verify against your DB driver first). Do not mix both.

No external I/O inside the transaction. Mail, HTTP calls, and dispatch of downstream jobs go through DB::afterCommit() or a step that runs after the transactional core. A rollback cannot un-send an email.

Validation lives at the step boundary, where it can fail loudly and early. 1.0.4 added step-level validation specifically so multi-step forms and approval gates don't grow hand-rolled guard clauses. Keep validation declarations next to the step they guard, and make sure the failure exception is one your controller/exception handler already renders as a 422 — not a 500.

Queue wrapper, not queue-per-step. Run flows inside a job rather than making the flow itself queue-aware:

final class RunOnboarding implements ShouldQueue
{
    public function handle(): void
    {
        app(OnboardingFlow::class)->run($this->payload);
    }
}

This keeps flow classes synchronous and trivially testable, while the job gets retries, backoff, and failed() handling from Laravel. Set $tries deliberately — a retried flow re-runs steps, so every step needs to be idempotent or the flow needs a resumable marker.

Wrap the package behind your own interface. One thin abstraction now saves a rewrite later if the package is abandoned:

interface RunsFlows
{
    public function run(Flow $flow, object $payload): object;
}

Controllers and jobs depend on RunsFlows, not on the package's base class. This is cheap insurance given the single-maintainer bus factor.

Test step by step, and test the failure paths first. Because steps are invokable classes, you can unit-test each in isolation with a hand-built payload, and integration-test the composed flow separately. Write these tests before adopting:

  • validation rejection at the flow entry point (nothing written)
  • rollback mid-flow (first two steps rolled back cleanly)
  • a step that throws (the exception surfaces, it isn't swallowed)
  • nested flow behavior (does the inner flow's transaction become a savepoint?)
  • job retry after a partial failure (no duplicate writes)

One public entry point per flow. Resist adding runSync(), runWithoutEmail(), or flags on the payload. If you need variants, write a second flow class. Flows are cheap to create and expensive to make conditional.

Gotchas and Tips

A patch release changed behavior. 1.0.4 carries transaction boundaries, stricter validation, and typing changes under a patch version. Treat it as a minor/major for upgrade purposes: read PR #5, run your flow test suite against the new version in a branch, and only then bump. Do not auto-upgrade this dependency in Renovate/Dependabot.

A forgotten return from a step fails silently. This is the number-one debugging trap with readonly payloads: the flow continues with a stale or null payload and blows up two steps later, far from the cause. Mitigate with strict return types, a null assertion at the top of each step, and — when debugging — a temporary no-op "spy" step that dd()s the payload between two real steps.

Nested transactions become savepoints, silently. A flow invoked inside another flow's transaction, or a flow that itself opens a transaction, will not roll back independently. The outer transaction controls everything, and a failed inner flow can leave a partially committed outer flow that looks successful. Decide explicitly: either flows never open their own transactions (caller owns the boundary), or flows always own their boundary and are never nested. Don't discover this in production.

Multiple DB connections. Framework transaction helpers default to the default connection. If a flow touches a second connection, its writes are outside the transaction and will not roll back. Document the connection for each flow, or split into two flows.

Queued flows + retries = duplicate side effects. A job that retries after a step succeeded but a later step failed will re-run the completed step. Make each step idempotent (upsert by business key, check-then-act) or record completed steps. This is a design obligation the package does not remove.

Generics are PHPDoc, not runtime enforcement. The 1.0.4 typing improvements pay off in PHPStan/Psalm and in review, but a wrong-typed payload will not throw at runtime. If you're relying on static analysis as your safety net, keep the baseline green in CI or the benefit evaporates.

Observability is yours to build. No run history, no replay, no dashboard, no built-in metrics. Cheap wins: log the flow class name and payload correlation ID at entry, log the step class name at each transition, and emit a single "flow completed/failed" event with duration. If you later need replay, those logs are the only thing that will let you reconstruct a run.

Bus factor is the standing risk. Single maintainer, zero dependents, no SLA, pre-2.0. Mitigations that cost almost nothing today: pin the version, wrap behind an interface, assign a named internal owner, and be prepared to vendor the package if a revenue-critical flow depends on it. Record that as an accepted decision rather than an unspoken one.

Know what to decline. In-process orchestration only. Cross-service sagas, durable timers, human task queues with SLAs, and anything wanting a visual builder should go to Temporal, Laravel Workflow, or a broker — not here. Saying so up front keeps the package from being over-sold internally and keeps the pattern honest.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource