the42coders/workflows
The42Coders Workflows lets you design and run database-driven workflows in Laravel through a visual editor. Build processes from start nodes, tasks, conditions, and more, then attach them to Eloquent models with a simple API—great for approvals, onboarding, and business process automation.
composer require the42coders/workflows
php artisan vendor:publish --tag=workflows
php artisan migrate
Publish both the config and migrations tags (and the assets tag if you want the bundled drag-and-drop editor UI); the exact tag list is in the package's service provider, so run php artisan vendor:publish interactively once and note what it offers before scripting it in CI.
Where to look first, in order:
config/workflows.php — the node-type registry. This is the heart of the package: it maps a string key (persisted on workflow_nodes.type) to a PHP class that does the work and an icon/colour for the editor. Everything you extend starts here.src/Models/Workflow.php, WorkflowNode.php, WorkflowRun.php — the persisted graph and the execution record. WorkflowRun is your audit log and your debugging surface.src/Nodes/ — the shipped node classes (mail, model update, condition, etc.). Read two or three before writing your own; they establish the expected method signatures and the convention for returning a value to the next node.web middleware at a /workflows prefix). Log in as an admin and drag a two-node flow before writing any code — it's the fastest way to understand how start_node_id, connections, and node config JSON fit together.First real use case: "when an order is paid, email a receipt and flip the order status." Make the Order model use the package's HasWorkflows trait, create a workflow with a trigger node bound to Order, a mail node, and an update node, then kick it off from the payment webhook:
use 42coders\Workers\Workflows\Models\Workflow; // namespace per your install
Workflow::start($order); // returns a WorkflowRun
If start() isn't on the facade in your version, the trait exposes the same entry point on the model. Confirm the call site against src/ before building on it — this is a small package and the public surface is thin enough to read in ten minutes.
Treat nodes as thin adapters over jobs. The single most important pattern for day-to-day sanity: the node resolves its config into a payload and dispatches a queued job. Keep branching logic in the graph, not in the node. This means a slow SMTP send or a third-party API call never blocks the request that started the workflow, and a failed node retries under your normal queue policy instead of dying with the HTTP request.
class SendReceiptNode extends Node
{
public function handle(): mixed
{
SendReceiptJob::dispatch($this->subject, $this->config('template'));
return $this->subject;
}
}
Start workflows from observers, not from controllers. One OrderObserver::paid() that calls start() is easier to audit than eight controllers each remembering to. Guard it so only one run exists per state transition — a paid_at that gets set twice on a retried webhook should not double-send receipts.
Use condition nodes as the only branch mechanism. Resist if statements inside action nodes; when someone asks "why didn't the refund email go out?", the answer needs to be visible in the graph, not buried in PHP. Converge branches back onto a shared node rather than duplicating downstream steps.
Version by copying. Editing a workflow that has live WorkflowRun rows means old runs replay against a graph that no longer matches what actually happened. Clone the workflow, edit the clone, repoint, and archive the original. Cheap, and it makes the audit log honest.
Integrate with the rest of the app through the subject, not through globals. Every node receives the model instance that started the run. Pass data forward by mutating the subject (status, timestamps) or by writing to your own domain tables — not by stuffing a shared array into the run record.
Test with fakes and assertions on the run. Queue::fake() plus Bus::fake() lets you assert that a workflow starting from a paid order dispatched the receipt job and flipped the status, without any SMTP. Assert on WorkflowRun rows (status, node log entries) for the graph shape, and on jobs for the side effects. Run one end-to-end test with the real queue on SQLite in-memory to catch serialisation problems.
Register custom nodes once, in config, with stable keys. The type string is persisted in the database forever. Name it after the domain concept (order.refund), never after the class name, so refactors don't orphan every existing workflow.
"minimum-stability" tweaks, a fork, or a repositories vcs entry pointing at a patched fork. With zero dependents on Packagist there's no crowd verifying compatibility for you — you are the compatibility test.DB::afterCommit() or dispatch a job from the observer so the run begins after the write lands.max_steps check in the runner) and fail the run loudly rather than letting the queue chew through it.handle(), with an explicit exception naming the node — a null template path three nodes deep is otherwise invisible.workflow_runs first: no row means the start never fired (bad trait, wrong class, observer not registered); a row with a stuck node means the graph or config is wrong.php artisan tinker access.public/vendor untouched.WorkflowRun (and any per-node log table) grows monotonically and gets wide. Schedule a nightly delete of runs older than 30–90 days, keeping failures longer if you rely on them for incident review.config:cache after registering a new node type, or the editor won't list it and existing nodes with that type will resolve to null on the next run.config/workflows.php, a custom Blade view per node for its config form in the editor, and the model trait for attaching workflows to additional entities. Everything else — retries, idempotency, notifying humans on failure — is yours to build; the package gives you a graph runner and an audit table, not an operations story.How can I help you explore Laravel packages today?