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

Workflow Engine Laravel Package

juststeveking/workflow-engine

A lightweight workflow engine for Laravel applications. Define workflows with states, transitions, guards, and actions to model approval or business processes, then move records through each step using a clean, expressive PHP API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its assets:

composer require juststeveking/workflow-engine
php artisan vendor:publish --tag="workflow-engine-config"
php artisan vendor:publish --tag="workflow-engine-migrations"
php artisan migrate

Where to look first, in order:

  1. config/workflow-engine.php — table names, model bindings, and any guard/event toggles. Most integration surprises live here.
  2. The published migrations — these define the shape of the workflow state and transition history tables. Read them before you design your own domain tables around them.
  3. The HasWorkflow-style trait (or contract) the package expects you to add to your model — that is your primary entry point.
  4. The package's tests directory on GitHub, which is usually more current than the README for exact method signatures.

First use case: attach the trait to an Eloquent model, declare its allowed states and transitions, then apply a named transition and let the package persist the resulting state plus the audit record.

use JustSteveKing\WorkflowEngine\Concerns\HasWorkflow;

class Order extends Model
{
    use HasWorkflow;

    protected function workflow(): array
    {
        return [
            'states' => ['draft', 'placed', 'paid', 'shipped'],
            'transitions' => [
                'place' => ['from' => 'draft', 'to' => 'placed'],
                'pay'   => ['from' => 'placed', 'to' => 'paid'],
            ],
        ];
    }
}

$order->apply('place');

Treat the config array as the single source of truth for legal moves — you should not be checking $order->status === 'draft' anywhere else in the codebase once this is wired up.

Implementation Patterns

Wrap transitions in a domain action, not a controller. Controllers should validate intent; the action owns the transition, so retries, jobs, and console commands all go through the same path:

final class PayOrder
{
    public function __construct(private PaymentGateway $gateway) {}

    public function handle(Order $order): Order
    {
        return DB::transaction(function () use ($order) {
            $this->gateway->charge($order);      // side effect first
            $order->apply('pay');                // then mutate state
            return $order->refresh();
        });
    }
}

Guard on intent, not on state. Put precondition checks in a policy or a canTransitionTo() call so the UI and the API share one rule set. Rejecting an illegal transition inside the package should be the backstop, not the first line of defence.

Listen for the package's transition events. They are the cleanest seam for notifications, webhooks, and analytics, and they fire once per successful transition rather than on every model save():

Event::listen(TransitionApplied::class, SendOrderReceipt::class);

Use the history table as your audit log. Rather than a parallel order_events table, read transitions back from the package's records and join them to your own metadata if the package exposes a meta/payload column.

Keep workflows in an enum for the state names. A backed enum gives you IDE completion, a single place to rename, and typed comparisons — while the package stores strings in the database layer.

Testing and factories. v1.1.2 makes the factory resolution extensible: a subclassed model can now declare its own factory rather than inheriting whatever the parent hardcoded. If you extend the package's base workflow/transition model — for example to add a tenant column, a custom table, or seeded state fixtures — give the subclass a factory:

class TenantWorkflow extends Workflow
{
    protected static function newFactory(): WorkflowFactory
    {
        return TenantWorkflowFactory::new();
    }
}

Before v1.1.2 this override was effectively ignored and your subclass would be instantiated through the base factory, producing rows without your custom attributes. If you subclass any package model, pin ^1.1.2 in composer.json.

Seed realistic state. In tests, drive models through the transitions themselves rather than setting the state column directly. It's slower but it exercises the guards and produces the history rows that your assertions and observers depend on.

Gotchas and Tips

  • Published migrations are yours to edit, vendor migrations are not. If you need a tenant column or an index, publish and amend. Editing the package's own copies will be silently overwritten on the next composer update.
  • Re-run php artisan config:cache after changing config. Transition definitions read from config will appear to be ignored in production if the cache is stale — a very common false bug report.
  • The package is not automatically transactional. Wrap side effects and the transition in one DB::transaction or you will eventually get a charged card against a placed order. Note the ordering in the example above: side effect inside the transaction, transition last, so a gateway exception leaves the state untouched.
  • State strings are unvalidated on write in many configurations. A typo in a config array produces a state that no transition can leave, and nothing throws until an hour later. Add a test that asserts every declared transition's from/to values exist in states.
  • Model observers can recurse. If a saved observer applies a transition, and that transition saves the model again, you get an infinite loop that only shows up under load. Prefer the package's transition events, which fire after commit semantics you control.
  • History growth. The audit table grows with every transition. Add a retention/pruning command early if your models are high-volume, and index on (model_type, model_id) if it isn't already indexed.
  • Concurrency. Two parallel requests can both read draft and both attempt place. Use lockForUpdate() inside the transaction, or the package's optimistic check if it exposes one, before calling apply().
  • Factory resolution subtleties. When overriding newFactory(), return an instance (Factory::new()), not a class-string, and make sure Factory::guessFactoryNamesUsing hasn't already resolved your subclass to the wrong namespace. If your factory produces the right class but the wrong state, the bug is in your states definition, not the factory.
  • Upgrade note for v1.1.2. The change is additive and non-breaking — nothing to migrate. The one behaviour to be aware of is that a subclass which previously relied on inheriting the parent factory will now honour its own if it declares one; if you have a subclass with a newFactory() that was written defensively against the old behaviour, re-run your test suite to confirm it still returns the expected models.
  • Extension points worth knowing: the transition events (for notifications and webhooks), the guard/condition hooks (for policy checks), the model bindings in config (for swapping in your own subclass), and — as of v1.1.2 — factory resolution on subclassed models.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource