jobmetric/laravel-flow
jobmetric/laravel-flow adds structured, code-driven workflows to Laravel: define flows, states, and transitions, move records through each stage, and track progress with events and database history. Great for approvals, order lifecycles, and multi-step business processes.
Install and publish:
composer require jobmetric/laravel-flow
php artisan vendor:publish --provider="Jobmetric\Flow\FlowServiceProvider"
php artisan migrate
Publish in this order: config, migrations, then run migrate. If you skip migrations the package will silently fail on every call — this is the #1 first-hour trap.
Where to look first:
config/flow.php — the shape of states, transitions, and any default workflow definitions live here. Skim it before touching code; most surprises come from defaults you didn't read.database/migrations/*_create_flow_* — confirms the exact table names and columns the package exposes. Reading these saves you from guessing at ->transitionTo() semantics later.HasFlow (or similarly named) trait in src/Traits/ — this is the public surface you attach to your own models. Everything else is plumbing.First use case — a status lifecycle on an existing model:
The canonical entry point is "I have an Order and it moves through pending → paid → shipped → delivered, and I want that lifecycle enforced and logged." Attach the trait, register the workflow, and let transitions go through the package instead of setting order->status = ... directly.
use Jobmetric\Flow\Traits\HasFlow;
class Order extends Model
{
use HasFlow;
}
Then, from a service or controller:
$order->transitionTo('paid', ['payment_id' => $payment->id]);
That single call should validate the transition is legal, persist it, and fire the package's transition event — which is where you hook side effects.
Models are for behavior; workflow graphs are data. Put workflow definitions in config/flow.php for static lifecycles (order status, invoice state) and in a seeder for anything DB-configurable (per-tenant approval chains). Keeps your model classes from accumulating arrays of states.
Never call transitionTo() blind. The idiomatic pattern is:
if (! $order->canTransitionTo('shipped')) {
abort(422, 'Order is not shippable yet.');
}
$order->transitionTo('shipped');
This makes your failure mode a domain error instead of an exception from inside the package, and it lets you give the user a meaningful message. Wrap both in a form request or action class so authorization happens outside the workflow layer.
The right place for "send the shipping email," "charge the card," or "notify the sales rep" is the package's transition event:
Event::listen(FlowTransitioned::class, function (FlowTransitioned $event) {
if ($event->flow->to === 'shipped') {
Mail::to($event->model->customer)->queue(new OrderShipped($event->model));
}
});
Keep the event listener side effects idempotent where possible — if your queue retries, you don't want two charge attempts. Check ->to state explicitly; a single listener catching all transitions is a debugging nightmare.
Workflows that touch more than one record (order + invoice + inventory) must be transactional:
DB::transaction(function () use ($order) {
$order->transitionTo('paid');
$order->invoice->transitionTo('paid');
});
The package persists the new state but won't roll back related models for you. If you don't wrap this, a crash between the two transitions leaves inconsistent data.
This package scales best when each model owns its own workflow. Don't try to model "Order approval → Inventory → Shipping" as one mega-workflow. Instead, let the Order workflow reach a fulfilled state and trigger a shipping workflow from the listener. Small, composable workflows are the whole point — hence "Make Work Flow your Way."
Create factory states for each workflow state so tests read like the domain:
$order = Order::factory()->paid()->create();
$order->transitionTo('shipped');
This keeps test setup aligned with the workflow definition (if you add a new prerequisite state, the factory state updates, not 40 tests).
Authorization belongs in Laravel policies, not in workflow guards. Use the workflow to answer "can this state change happen?" and the policy to answer "can this user cause it?". Trying to encode roles into workflow definitions is a common early mistake and it doesn't scale.
Because workflow definitions are read from config, php artisan config:cache will freeze them for the process. If you change config/flow.php and nothing happens in production-like environments, run php artisan config:clear first. This is the single most common "the package is broken" false alarm.
If vendor:publish runs the config but not the migrations (because you were asked "which provider?" and picked wrong), everything looks installed but migrate creates no tables. The package will only fail on the first transitionTo(). Confirm the tables exist (php artisan db:show --table=flows) before debugging anything else.
transitionTo() from an unexpected state is silent or loud depending on versionSome releases throw FlowTransitionException; others return false. Never assume. Check canTransitionTo() first and treat the result of transitionTo() as advisory. If you rely on exceptions and a version bump relaxes it, you get silent no-ops in production.
The package likely exposes guard/condition hooks on transitions. Don't put business rules in both guards and event listeners. Guards should be pure checks (does the required field exist?), listeners should be effects (send mail, hit webhook). Mixed responsibilities make workflow bugs almost unfindable.
Two requests arriving simultaneously on paid → shipped can both pass a pre-check and both transition. If that matters, wrap in DB::transaction with a lockForUpdate() read of the record first, or add a unique index on (model_id, to_state) in the flows table. This is a subtle bug that only shows up under load.
If the package doesn't support a feature you need (e.g., transition reasons, actor IDs), add your own HasFlow trait that uses the package trait and overrides the transition method, then attach your trait to models. Forking loses you upgrades; the trait composition pattern is what the package is designed for.
With fewer than 30 stars and a niche scope, expect sparse docs, few Stack Overflow answers, and a limited changelog. Budget time to read src/ — specifically the trait and the transition service — before assuming a feature exists. When you find a behavior that isn't documented, add a comment in your own wrapper trait so the next developer on your team doesn't rediscover it.
Even if the package stores transitions, also let your application listen and record them in an audit table or activity log. Packages change; audit history is a compliance asset. The transition event is your one cheap hook to capture actor, IP, reason, and timestamp in a schema you control.
How can I help you explore Laravel packages today?