Workflow Engines

Modelling Complex Business Processes in Code


Every business runs on processes. Orders flow through approval chains. Support tickets escalate based on rules. Content moves through review stages with handoffs and sign-offs at each step.

Most of these processes start in spreadsheets or shared documents, and they work well enough until they do not. The moment a business needs to enforce rules consistently, track where things are in a pipeline, or handle exceptions without manual intervention, it needs proper workflow automation.

We have been building custom workflow engines since 2005, and have used Laravel for them since the framework's early days, across 50+ applications. What follows is how we approach the problem: the patterns that work, the traps that waste time, and the decisions that matter when modelling business processes in code.

Fulfilment workflow topology
Cross-departmental handoffs and friction points
Customer Sales Ops Finance
Customer
Sales
Ops
Finance
01. Trigger

Inquiry Received

02. Field Ops

Site Survey

03. Sales

Prepare Quote

REJECTED
04. Customer

Review Proposal

05. Sales

Revision Logic

06. Decision

Quote Accepted

07. Finance

50% Deposit

DELAY
08. Logistics

Order Materials

09. Execution

Installation

10. Finance

Balance Invoice


What is a workflow engine?

A workflow engine is a software component that manages the movement of work through defined stages. It tracks the current state of each item, enforces rules about what can happen next, and triggers actions when transitions occur.

In practical terms, a workflow engine answers three questions at any point in a process:

Where is this item now?

Its current state. A workflow engine always knows the exact position of every item in the pipeline.

What can happen to it?

Available transitions. Based on the current state, guards, and user permissions, the engine knows which moves are valid.

What happens when it moves?

Actions and side effects. Notifications, inventory updates, task creation: all triggered automatically on transition.

The distinction from simple status tracking is important. A status field records where something is. A workflow engine controls how it gets there and what happens along the way.

This is the foundation of business process automation: encoding your operational rules into a system that enforces them consistently, whether it is processing the first order of the day or the thousandth.


Why naive implementations fail

The pattern we see most often starts reasonably. A developer adds a status field to a database table, writes a few conditional checks, and the workflow appears to work.

class PurchaseOrder extends Model
{
    public function submit(): void
    {
        if ($this->status !== 'draft') {
            throw new InvalidStateException('Can only submit drafts');
        }

        if ($this->total > 10000) {
            $this->status = 'pending_director_approval';
        } else {
            $this->status = 'pending_manager_approval';
        }

        $this->save();
        Mail::send(new OrderSubmitted($this));
    }
}

This works for the first version. Then real business rules arrive:

"But sometimes we skip that step"
"Unless it is over 10,000 pounds, then it needs director approval"
"We can reverse it, but only within 24 hours"
"If it is from a preferred supplier, the payment terms are different"

Each exception spawns an if-statement. Each if-statement interacts with others. Within months, the transition logic is scattered across controllers, models, event listeners, and scheduled tasks. Nobody can confidently answer "what happens when this order moves from pending to approved?"

The cost of scattered workflow logic:

Brittle systems needing constant developer intervention. Workarounds outside the system. Shadow processes reverting to spreadsheets and email chains. Lost audit trails from direct database updates bypassing business rules. Untestable logic scattered across dozens of files. Fear of change because nobody knows the full set of side effects.

If this sounds familiar, it is worth mapping your processes visually before writing any code. Understanding the full picture prevents building on incorrect assumptions.


The state machine pattern for workflow automation

The fix is to make the implicit state machine explicit. Rather than scattering transition logic, define it in one place with four clear components.

States

Named conditions an entity can be in. For a single-track state machine, states are exhaustive and mutually exclusive: an order is either draft, pending_approval, approved, or rejected. It cannot be two at once. (Broader workflow models support parallel states and subprocesses, but most Laravel applications start with a single-track machine.)

Transitions

Named movements between states. Each transition has a source state (or set of source states), a target state, and a name. "Submit" moves a purchase order from draft to pending_approval.

Guards

Functions that determine whether a transition is allowed right now. Guards check preconditions without changing anything: user permissions, data validity, business rules, time constraints. If a guard returns false, the transition is blocked.

Actions

Side effects triggered when a transition fires. Send an email. Create a task. Update inventory. Log an audit entry. Actions run after the transition succeeds, keeping them separate from the decision about whether the transition should happen. Critical actions (like inventory changes) should run inside the same database transaction as the state change; non-critical ones (like notifications) should be dispatched to a queue after the transaction commits (using afterCommit or a transactional outbox) so they only fire if the state change actually persists.

// Workflow configuration - single source of truth
return [
    'states' => [
        'draft',
        'pending_manager_approval',
        'pending_director_approval',
        'approved',
        'rejected',
        'cancelled',
    ],
    'transitions' => [
        'submit_standard' => [
            'from' => ['draft'],
            'to' => 'pending_manager_approval',
            'guards' => [HasRequiredFieldsGuard::class, BelowDirectorThresholdGuard::class],
            'actions' => [NotifyManagerAction::class],
        ],
        'submit_high_value' => [
            'from' => ['draft'],
            'to' => 'pending_director_approval',
            'guards' => [HasRequiredFieldsGuard::class, AboveDirectorThresholdGuard::class],
            'actions' => [NotifyDirectorAction::class],
        ],
        'approve' => [
            'from' => ['pending_manager_approval', 'pending_director_approval'],
            'to' => 'approved',
            'guards' => [UserHasApprovalPermissionGuard::class],
            'actions' => [
                DeductInventoryAction::class,
                NotifyRequesterAction::class,
                CreateFulfilmentTaskAction::class,
            ],
        ],
        'reject' => [
            'from' => ['pending_manager_approval', 'pending_director_approval'],
            'to' => 'rejected',
            'guards' => [UserHasApprovalPermissionGuard::class],
            'actions' => [NotifyRequesterAction::class],
        ],
    ],
];

The entire workflow is readable in one file. Guards and actions are independent, testable classes. Adding a new transition or changing a guard does not require hunting through the codebase.

Multi-level approval workflows in practice

Simple two-step approvals are straightforward. Real business process automation gets interesting when approval chains branch, loop, and escalate.

Consider a content approval workflow for marketing materials with states: Draft, Pending brand review, Pending legal review, Pending director review, Changes requested, Approved, Published, Withdrawn. Routing conditions determine which path an item takes through the workflow. (Note: some engines distinguish guards, which block a transition, from routing conditions, which select a branch. The principle is the same: isolated, testable rules.)

// Routing condition - determines whether an item needs legal review.
// Illustrative: real implementations combine keyword scanning with
// structured metadata, rule tables, and reviewer override.
class RequiresLegalReviewCondition implements RoutingCondition
{
    private array $triggerWords = [
        'guaranteed', 'certified', 'compliant',
        'regulation', 'patent', 'trademark',
    ];

    public function evaluate(Entity $entity): bool
    {
        $content = strtolower($entity->content);
        return collect($this->triggerWords)
            ->contains(fn($word) => str_contains($content, $word));
    }
}

Content containing legal trigger words routes through legal review. Content above a spend threshold routes through director approval. Reviewers can request changes, sending the item back to draft with revision notes attached. This is the kind of workflow that breaks naive implementations. With an explicit state machine, each path through the process is named, tested, and traceable.

Concurrency and idempotency

In production, two users might attempt the same transition at the same time. A queued job might fire twice. An approval webhook might arrive while a scheduled escalation is already running.

Guard against these with optimistic locking on the workflow instance (check the expected current_state in the UPDATE query), unique constraints or idempotency keys on transitions, and actions that are safe to retry. If an action has already taken effect (inventory deducted, notification sent), repeating it should be a no-op, not a duplicate. Without these protections, a workflow that works in development will produce inconsistent state under real load.


Handling long-running and persistent workflows

Not every workflow completes in a single request. Purchase orders wait for approvals. Onboarding processes span weeks. Contract renewals sit in review for months. Long-running workflows need three things naive implementations rarely handle well.

State persistence

The workflow state lives in the database, not in application memory:

CREATE TABLE workflow_instances (
    id BIGINT PRIMARY KEY,
    entity_type VARCHAR(255),
    entity_id BIGINT,
    current_state VARCHAR(100),
    workflow_version INT DEFAULT 1,
    lock_version INT DEFAULT 0,        -- optimistic locking
    entered_state_at TIMESTAMP,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    UNIQUE (entity_type, entity_id)
);

CREATE TABLE workflow_transitions (
    id BIGINT PRIMARY KEY,
    workflow_instance_id BIGINT,
    from_state VARCHAR(100),
    to_state VARCHAR(100),
    transition_name VARCHAR(100),
    idempotency_key VARCHAR(255) UNIQUE, -- prevents duplicate transitions
    triggered_by BIGINT,
    metadata JSON,
    created_at TIMESTAMP
);

The transitions table creates a strong foundation for auditability. Every state change records who triggered it, when, and any associated metadata. This matters for debugging and understanding how processes actually flow in practice. For full compliance, you will also need access control, segregation of duties, retention policies, and evidence that the audit log itself cannot be tampered with.

Scheduled transitions

Some transitions happen on a clock, not on a user action. Auto-escalation after 48 hours of inactivity. Reminder notifications approaching a deadline. Automatic cancellation of abandoned requests. These run as background jobs on a schedule, checking for workflow instances that meet time-based criteria and firing transitions if the guards allow.

Workflow versioning and migration

Business processes change. Approval thresholds shift. New review stages get added. Old states become irrelevant. The question is what happens to items already in the pipeline when the workflow definition changes.

Additive changes are usually lowest-risk. Adding a new state or transition from an existing state rarely breaks items already in progress. They gain new options. Be aware that new auto-transitions, UI surfaces, or reporting queries may still change behaviour for in-flight items.
Breaking changes require a migration strategy. If you remove a state that items currently occupy, you need to tag each workflow instance with a version number, keep old workflow definitions available, provide explicit state mapping for migrations, and run the migration as a batch operation with logging.

A version field on the workflow instance costs nothing to add upfront and prevents painful retrofitting later.


Monitoring and debugging workflow systems

A workflow management system is only useful if you can see what is happening inside it.

What to track

State distribution

How many items are in each state right now? A growing pile in pending_approval signals a bottleneck.

Time in state

How long do items spend in each state on average? If pending_legal_review averages 12 days while pending_brand_review averages 2, legal review is the constraint.

Transition success rate

What percentage of transition attempts succeed? A high failure rate on a specific transition suggests the guards are too strict or the UI is allowing users to attempt invalid actions.

The transitions table records completed state changes. Separately, log every transition attempt (including blocked ones) to your application logs or a dedicated attempts table. When a transition is blocked by a guard, record which guard blocked it and why. This creates an operational record that answers "why is this order stuck?" without requiring a developer to trace through code.

Testing workflow logic

State machine workflows are significantly easier to test than scattered conditional logic because each component is isolated.

public function test_approval_limit_guard_blocks_high_value_orders(): void
{
    $order = PurchaseOrder::factory()->create(['total' => 15000]);
    $guard = new WithinApprovalLimitGuard(limit: 10000);

    $this->assertFalse($guard->check($order));
}

public function test_approved_order_creates_fulfilment_task(): void
{
    $order = PurchaseOrder::factory()
        ->inState('pending_approval')
        ->create();

    $this->workflow->apply($order, 'approve');

    $this->assertEquals('approved', $order->currentState());
    $this->assertDatabaseHas('tasks', [
        'entity_id' => $order->id,
        'type' => 'fulfilment',
    ]);
}

Scenario tests walk through complete paths, verifying that a purchase order can move from draft through approval to fulfilment, or that a rejected order can be revised and resubmitted.


When to build custom vs adopt a platform

This is the most common question we hear. The answer depends on three factors.

Factor Build custom Adopt a platform
Data model Deeply integrated with your data (orders, invoices, customer records) Workflows are largely standalone from your core data
UI control Full control over user interface and experience needed Visual workflow editors (BPMN-style) needed by business users
Change frequency Rules change frequently, developers available to maintain them Business users need to modify workflows without developers
Compliance Compliance achievable in code Need audit certification and process documentation out of the box

Laravel ecosystem packages (like Symfony Workflow, which integrates cleanly with Laravel, plus community state machine libraries) give you the pattern without building from scratch. Combined with Laravel's queues and events, this covers most custom workflow automation projects.

Dedicated platforms make sense when workflows become the primary complexity of the system rather than a feature within it. Camunda focuses on BPMN modelling and human task orchestration, suitable when business users need visual workflow editors. Temporal provides code-first durable execution, better suited to long-running processes that span multiple services and need built-in retry and compensation.


The business case for proper workflow automation

When workflow automation is implemented well, the business impact compounds.

  • Consistency The same rules apply to every item, every time. No more "it depends who processes it."
  • Visibility Anyone can see where any item is in the pipeline and what needs to happen next. This alone eliminates a category of status meetings and email chasing.
  • Auditability Every state change is logged with who, when, and why. Gathering audit evidence starts with a query rather than a forensic exercise across scattered systems.
  • Maintainability Changing a business rule means updating a guard or adding a transition, not hunting through scattered conditionals.
  • Scalability The same workflow definition handles ten items or ten thousand. With proper indexing, queueing, and partitioning, throughput can scale well beyond what scattered logic allows.

For order management workflows, this means tracking every item from enquiry to delivery without dropping balls between departments. For service delivery systems, it means consistent quality regardless of which team member handles the work.


Frequently asked questions

What is the difference between a workflow engine and a state machine?

A state machine defines states and the transitions between them. A workflow engine is a broader system that uses a state machine at its core but adds persistence, scheduling, human task management, notifications, and monitoring. In practice, most custom workflow automation starts with a state machine and grows into a full workflow engine as operational needs emerge.

How long does it take to build a custom workflow engine?

For a single entity type with one approval path, limited integrations, and an existing application to build on, a basic state machine with guards, actions, and persistence typically takes two to four weeks. The timeline extends with complexity: multi-level approvals, scheduled transitions, and workflow versioning each add time. Every project is different, so we scope carefully before committing to a timeline.

Can workflow engines handle approval processes across multiple departments?

Yes. Multi-departmental approval workflows are one of the most common use cases. The state machine routes items to different review queues based on routing conditions and guards (value thresholds, content type, department rules), and each department sees only the items requiring their attention.


Automate your workflows

If your business processes have outgrown spreadsheets and scattered logic, we should talk. We will walk through your workflows together. No pressure, no jargon, just a practical conversation about what a proper workflow automation system could look like for your operations.

Book a discovery call →
Graphic Swish