PHPackages                             maestrodimateo/laravel-workflow - 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. maestrodimateo/laravel-workflow

ActiveLibrary

maestrodimateo/laravel-workflow
===============================

Workflow engine for Laravel applications

v1.3.6(1mo ago)116↑462.5%MITBladePHP ^8.3 || ^8.4

Since Apr 3Pushed 1w agoCompare

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

READMEChangelogDependencies (6)Versions (13)Used By (0)

Laravel Workflow
================

[](#laravel-workflow)

[![Latest Version on Packagist](https://camo.githubusercontent.com/2bc75d724ad67ba64e9a3c76c1b500903f63df03326d91477fce1cd27b7911f9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d61657374726f64696d6174656f2f6c61726176656c2d776f726b666c6f772e737667)](https://packagist.org/packages/maestrodimateo/laravel-workflow)[![License](https://camo.githubusercontent.com/88936cc3c45e63970297197fadd5da0bf3aac089549818413872575f8944d693/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6d61657374726f64696d6174656f2f6c61726176656c2d776f726b666c6f772e737667)](https://packagist.org/packages/maestrodimateo/laravel-workflow)

A visual, configurable workflow engine for Laravel. Define circuits (workflow definitions), baskets (steps), and transitions — then move any Eloquent model through them with a clean Facade API.

Comes with a **built-in visual admin interface** to design your workflows by drag-and-drop.

---

Features
--------

[](#features)

- **Visual workflow designer** — drag-and-drop baskets, draw transitions, configure actions
- **Facade &amp; helper** — `Workflow::for($model)->transition($basketId)` or `workflow($model)->transition($basketId)`
- **Multi-circuit** — a model can belong to multiple workflows, scoped with `in($circuit)`
- **Resource locking** — prevent concurrent access with `lock()` / `unlock()`
- **Role-based access** — define allowed roles per circuit and per basket
- **Transition actions** — attach actions (email, webhook, log, require documents, custom) to transitions
- **Duration tracking** — automatic timing between steps with human-readable formatting
- **Full history** — every transition is logged with who, when, how long, and why
- **Message templates** — WYSIWYG editor with variable interpolation
- **Export / Import** — share workflows as JSON files + PNG image export
- **Dark mode** — the admin UI supports light and dark themes
- **Zero build step** — the admin UI uses Alpine.js + Tailwind CDN, no npm required

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12 or 13

---

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

[](#installation)

```
composer require maestrodimateo/laravel-workflow
```

Publish the config and migrations:

```
php artisan vendor:publish --tag=workflow-config
php artisan vendor:publish --tag=workflow-migrations
php artisan migrate
```

Optionally publish the views to customize the admin UI:

```
php artisan vendor:publish --tag=workflow-views
```

---

Quick Start
-----------

[](#quick-start)

### 1. Add the trait to your model

[](#1-add-the-trait-to-your-model)

```
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Maestrodimateo\Workflow\Traits\Workflowable;

class Invoice extends Model
{
    use HasUuids, Workflowable;
}
```

When an `Invoice` is created, it's automatically placed in the **DRAFT** basket of the circuit targeting it.

### 2. Open the visual designer

[](#2-open-the-visual-designer)

Navigate to `/workflow/admin` in your browser. From there you can:

- Create a **circuit** (workflow) targeting your model
- Add **baskets** (steps) with colors and roles
- Draw **transitions** (links) between baskets by dragging from one to another
- Configure **actions** on each transition (send email, call webhook, etc.)

### 3. Transition models in your code

[](#3-transition-models-in-your-code)

```
use Maestrodimateo\Workflow\Facades\Workflow;

// Get current status
$basket = Workflow::for($invoice)->currentStatus();
echo $basket->name;   // "Brouillon"
echo $basket->status; // "DRAFT"

// See available next steps
$options = Workflow::for($invoice)->nextBaskets();

// Transition
Workflow::for($invoice)->transition(
    $nextBasket->id,
    'Approved by manager',  // optional comment
);
```

The `workflow()` helper is also available:

```
workflow($invoice)->currentStatus();
workflow($invoice)->transition($basketId);
```

---

Facade API Reference
--------------------

[](#facade-api-reference)

All methods are available via `Workflow::` or `workflow()->`.

### Model-bound methods

[](#model-bound-methods)

These methods require `Workflow::for($model)` first:

```
$wf = Workflow::for($model);
```

MethodReturnsDescription`in($circuit)``WorkflowManager`Scope to a specific circuit (required for multi-circuit)`currentStatus()``?Basket`Current basket (step) of the model`nextBaskets()``Collection`Available baskets to transition to`transition($id, $comment)``bool`Move the model to the next basket`history()``Collection`Full transition history with durations`totalDuration()``int`Total processing time in seconds`durationInStatus($status)``int`Time spent in a specific status (seconds)`allStatuses()``array`Current basket per circuit`circuits()``Collection`All circuits the model belongs to`lock($minutes)``WorkflowLock`Lock the model for exclusive access`unlock($force)``void`Release the lock`isLocked()``bool`Check if locked`isLockedByMe()``bool`Check if locked by the current user`lockedBy()``?string`User ID holding the lock`lockExpiration()``?Carbon`When the lock expires`requiredDocuments($basketId)``array`Documents required for a transition`requirements()``array`All requirements for all next transitions### Static methods

[](#static-methods)

These methods don't require `for()`:

MethodReturnsDescription`importFromJson($path)``Circuit`Import a circuit from an exported JSON file (for seeders/commands)`registerAction($class)``void`Register a custom transition action`getRegisteredActions()``array`List all registered action classes### Role-based queries

[](#role-based-queries)

These methods don't require `for()`:

```
// Circuits accessible to a role
Workflow::circuitsForRole('manager');
Workflow::circuitsForRoles(['admin', 'manager']);

// Baskets accessible to a role (optionally scoped to a circuit)
Workflow::basketsForRole('validator');
Workflow::basketsForRole('validator', $circuitId);
Workflow::basketsForRoles(['admin', 'operator'], $circuitId);
```

Eloquent scopes are also available directly:

```
use Maestrodimateo\Workflow\Models\Circuit;
use Maestrodimateo\Workflow\Models\Basket;

Circuit::forRole('admin')->get();
Basket::forRoles(['admin', 'manager'])->get();

$basket->hasRole('validator'); // true/false
$circuit->hasRole('admin');    // true/false
```

---

Duration Tracking
-----------------

[](#duration-tracking)

Every transition automatically records the time spent in the previous step:

```
$history = Workflow::for($invoice)->history();

foreach ($history as $entry) {
    echo $entry->previous_status;  // "DRAFT"
    echo $entry->next_status;      // "REVIEW"
    echo $entry->duration_seconds; // 3600
    echo $entry->duration_human;   // "1h"
    echo $entry->done_by;          // User ID
    echo $entry->comment;          // "Sent for review"
}

// Total time
$seconds = Workflow::for($invoice)->totalDuration();

// Time in a specific step
$reviewTime = Workflow::for($invoice)->durationInStatus('REVIEW');
```

Human-readable formats: `45s`, `12min`, `2h 35min`, `3j 4h`.

---

Multi-Circuit Support
---------------------

[](#multi-circuit-support)

A model can belong to multiple circuits simultaneously. Use `in()` to scope operations:

```
// Scope to a specific circuit
Workflow::for($invoice)->in($approvalCircuit)->currentStatus();
Workflow::for($invoice)->in($complianceCircuit)->transition($basketId);
Workflow::for($invoice)->in('circuit-uuid')->history();

// See status in ALL circuits at once
$statuses = Workflow::for($invoice)->allStatuses();
// [
//     'circuit-a-id' => ['circuit' => Circuit, 'basket' => Basket],
//     'circuit-b-id' => ['circuit' => Circuit, 'basket' => Basket],
// ]

// List circuits the model belongs to
$circuits = Workflow::for($invoice)->circuits();
```

When a model is created, it's automatically attached to the DRAFT basket of **every** circuit targeting its class.

---

Resource Locking
----------------

[](#resource-locking)

Prevent multiple operators from working on the same model simultaneously:

```
// Lock the model (default: 30 minutes)
Workflow::for($invoice)->lock();
Workflow::for($invoice)->lock(60); // 1 hour

// Check lock status
Workflow::for($invoice)->isLocked();       // true
Workflow::for($invoice)->isLockedByMe();   // true
Workflow::for($invoice)->lockedBy();       // "user-uuid"
Workflow::for($invoice)->lockExpiration(); // Carbon instance

// Transition (auto-checks the lock)
Workflow::for($invoice)->transition($basketId);
// → OK if you hold the lock (lock is released after transition)
// → ModelLockedException if locked by someone else

// Release manually
Workflow::for($invoice)->unlock();

// Admin force unlock
Workflow::for($invoice)->unlock(force: true);
```

### Query scopes

[](#query-scopes)

```
// Available models (not locked or lock expired)
Invoice::fromBasket($reviewBasket)->unlocked()->get();

// Models I'm working on
Invoice::lockedBy(auth()->id())->get();
```

### Handling lock exceptions

[](#handling-lock-exceptions)

```
use Maestrodimateo\Workflow\Exceptions\ModelLockedException;

try {
    Workflow::for($invoice)->transition($basketId);
} catch (ModelLockedException $e) {
    return back()->withErrors([
        'lock' => $e->getMessage(),
        // "Ce dossier est verrouillé par [user] jusqu'à [14:30]."
    ]);
}
```

Configure the default lock duration in `.env`:

```
WORKFLOW_LOCK_DURATION=30  # minutes
```

---

Transition Actions
------------------

[](#transition-actions)

Actions are executed automatically when a specific transition occurs. They are **configured visually** in the admin UI.

### Built-in actions

[](#built-in-actions)

ActionKeyConfigSend email`send_email`Select a message from the circuitLog`log`Optional messageWebhook`webhook`URL to POST toRequire documents`require_document`List of documents (type + label)### Custom actions

[](#custom-actions)

Generate a new action with artisan:

```
php artisan make:workflow-action GeneratePdfAction
```

This creates `app/Workflow/Actions/GeneratePdfAction.php`:

```
use Maestrodimateo\Workflow\Contracts\TransitionAction;

class GeneratePdfAction implements TransitionAction
{
    public static function key(): string { return 'generate_pdf'; }
    public static function label(): string { return 'Generate Pdf'; }

    public function execute(Model $model, Basket $from, Basket $to, array $config = []): void
    {
        // Your logic here
    }
}
```

Register it in your `AppServiceProvider::boot()`:

```
Workflow::registerAction(GeneratePdfAction::class);
```

The action immediately appears in the admin UI's "Add action" menu on any transition.

---

Events
------

[](#events)

A `TransitionEvent` is fired after every transition. Add your own listeners:

```
// In your EventServiceProvider
protected $listen = [
    \Maestrodimateo\Workflow\Events\TransitionEvent::class => [
        \App\Listeners\NotifySlack::class,
        \App\Listeners\SyncWithExternalSystem::class,
    ],
];
```

```
public function handle(TransitionEvent $event): void
{
    $event->currentBasket; // Source basket
    $event->nextBasket;    // Target basket
    $event->model;         // The transitioned model
    $event->comment;       // Transition comment
}
```

### What happens during a transition

[](#what-happens-during-a-transition)

```
1. Lock guard — throws ModelLockedException if locked by another user
2. Model detached from current basket, attached to next
3. Transition actions executed (configured visually on the link)
4. TransitionEvent fired → HistoryListener records history with duration
5. Lock released automatically
6. Your custom listeners run

```

---

Message Templates
-----------------

[](#message-templates)

Messages are created at the circuit level and can be used in transition actions (e.g., `send_email`).

The WYSIWYG editor supports **variable interpolation**:

```
Bonjour, la demande {{ reference }} a été transférée de {{ from_name }}
vers {{ to_name }} par {{ user }} le {{ datetime }}.

```

### Built-in variables

[](#built-in-variables)

VariableDescription`{{ model_id }}`Model identifier`{{ model_type }}`Model class name`{{ from_status }}` / `{{ from_name }}`Source basket`{{ to_status }}` / `{{ to_name }}`Target basket`{{ circuit_name }}`Circuit name`{{ date }}` / `{{ heure }}` / `{{ datetime }}`Current date/time`{{ user }}`User performing the transition### Custom variables

[](#custom-variables)

```
// config/workflow.php
'message_variables' => [
    'reference' => fn ($model) => $model->reference,
    'montant'   => fn ($model) => number_format($model->amount, 2, ',', ' ') . ' €',
],
```

---

Export / Import
---------------

[](#export--import)

### In the admin UI

[](#in-the-admin-ui)

- **Export JSON** — download the full circuit definition as a `.json` file
- **Export PNG** — download a high-resolution image of the workflow diagram
- **Import** — select a `.json` file to recreate a circuit with all its configuration

### Via API

[](#via-api)

```
GET  /workflow/admin/api/circuits/{circuit}/export
POST /workflow/admin/api/circuits/import  # multipart form, field: "file"
```

### Programmatic import (seeders, commands)

[](#programmatic-import-seeders-commands)

Import a previously exported JSON file directly from code — no HTTP request needed:

```
use Maestrodimateo\Workflow\Facades\Workflow;

// In a seeder
Workflow::importFromJson(database_path('seeders/workflow-invoices.json'));

// Or via the manager directly
app(\Maestrodimateo\Workflow\WorkflowManager::class)::importFromJson($path);
```

The method creates the full circuit (baskets, transitions, messages) inside a database transaction and returns the newly created `Circuit` instance with all relations loaded.

Throws `\InvalidArgumentException` if the file is missing or has an invalid format.

---

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

[](#configuration)

```
// config/workflow.php
return [
    'routes' => [
        'prefix'           => 'workflow',
        'middleware'        => ['api'],
        'admin_middleware'  => ['web'],
    ],
    'auth_identifier' => 'id',
    'message_variables' => [],
    'lock' => [
        'duration_minutes' => 30,
    ],
];
```

---

Workflowable Trait
------------------

[](#workflowable-trait)

The `Workflowable` trait adds relations, methods, and scopes directly on your model instance. Everything below is available without using the Facade.

### Relations

[](#relations)

```
$invoice->baskets;        // Collection — all baskets the model is/has been in
$invoice->histories;      // Collection — all transition history entries
$invoice->workflowLock;   // WorkflowLock|null — the active lock on this model
```

### Methods

[](#methods)

```
// Current status (last basket attached)
$invoice->currentStatus();                // ?Basket — across all circuits
$invoice->currentStatus($circuit);        // ?Basket — in a specific circuit
$invoice->currentStatus('circuit-uuid');   // same with a string ID

// Inspect the current basket
$invoice->currentStatus()->status;        // "REVIEW"
$invoice->currentStatus()->name;          // "Under Review"
$invoice->currentStatus()->color;         // "#2563eb"
$invoice->currentStatus()->roles;         // ["manager", "validator"]
$invoice->currentStatus()->hasRole('manager'); // true

// Access the circuit
$invoice->currentStatus()->circuit->name; // "Invoice Approval"

// Navigate the workflow graph
$invoice->currentStatus()->next;          // Collection — possible next steps
$invoice->currentStatus()->previous;      // Collection — where it came from

// History with duration tracking
$invoice->histories->each(function ($h) {
    $h->previous_status;   // "DRAFT"
    $h->next_status;       // "REVIEW"
    $h->comment;           // "Sent for review"
    $h->done_by;           // "user-uuid"
    $h->duration_seconds;  // 3600
    $h->duration_human;    // "1h"
    $h->created_at;        // Carbon
});

// Lock info
$invoice->workflowLock?->locked_by;      // "user-uuid" or null
$invoice->workflowLock?->expires_at;     // Carbon or null
$invoice->workflowLock?->isActive();     // true if not expired
```

### Scopes

[](#scopes)

```
// Models currently in a specific basket
Invoice::fromBasket($reviewBasket)->get();

// Available models (not locked or lock expired)
Invoice::unlocked()->get();

// Models locked by a specific user
Invoice::lockedBy(auth()->id())->get();

// Combine scopes
Invoice::fromBasket($reviewBasket)->unlocked()->get();
```

### Automatic Behavior

[](#automatic-behavior)

When a model is created, it's automatically attached to the DRAFT basket of every circuit targeting its class:

```
$invoice = Invoice::create(['number' => 'INV-001']);

$invoice->currentStatus()->status; // "DRAFT" — automatic
$invoice->baskets->count();        // 1 (or more if multiple circuits)
```

---

Admin Interface
---------------

[](#admin-interface)

The visual designer at `/workflow/admin` provides:

- **Circuit management** — create, edit, delete circuits with role assignment
- **Drag-and-drop canvas** — position baskets freely, auto-layout button
- **Visual linking** — click output port, then click target basket to create transitions
- **Transition config** — click a link to add label and actions
- **Message editor** — WYSIWYG editor with variable interpolation
- **Export / Import** — JSON + PNG export, JSON import
- **Zoom** — scroll wheel + controls
- **Dark mode** — toggle between light and dark themes
- **No build step** — works out of the box, powered by Alpine.js + Tailwind CDN

---

Testing
-------

[](#testing)

```
composer test
```

Or with Pest directly:

```
./vendor/bin/pest
```

---

License
-------

[](#license)

MIT. See [LICENSE](LICENSE) for details.

---

Credits
-------

[](#credits)

- [Noel Mebale](https://github.com/maestrodimateo)

###  Health Score

45

—

FairBetter than 92% of packages

Maintenance96

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity58

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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 ~0 days

Total

12

Last Release

30d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/92a2c64304e341345f2854b5bc9c43806b9e54f0b5b9b57135d37d69dd7c5c67?d=identicon)[mebalenoel](/maintainers/mebalenoel)

---

Top Contributors

[![maestrodimateo](https://avatars.githubusercontent.com/u/40523415?v=4)](https://github.com/maestrodimateo "maestrodimateo (26 commits)")

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/maestrodimateo-laravel-workflow/health.svg)

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

###  Alternatives

[mongodb/laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel

7.1k7.2M71](/packages/mongodb-laravel-mongodb)[laravel/pulse

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

1.7k12.1M99](/packages/laravel-pulse)[watson/validating

Eloquent model validating trait.

9723.3M46](/packages/watson-validating)[cybercog/laravel-ban

Laravel Ban simplify blocking and banning Eloquent models.

1.1k651.8k11](/packages/cybercog-laravel-ban)[dyrynda/laravel-model-uuid

This package allows you to easily work with UUIDs in your Laravel models.

4802.8M8](/packages/dyrynda-laravel-model-uuid)[fumeapp/modeltyper

Generate TypeScript interfaces from Laravel Models

196277.9k](/packages/fumeapp-modeltyper)

PHPackages © 2026

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