# Workflow for Laravel

# Summary

Chevere Workflow (opens new window) for Laravel is a PHP library for building and executing multi-step procedures with automatic dependency resolution. Define independent jobs that can run synchronously or asynchronously, pass data between them using typed responses, and let the engine handle execution order automatically.

Key features:

  • Declarative job definitions: Define what to do, not how to orchestrate it
  • Automatic dependency graph: Jobs execute in optimal order based on their dependencies
  • Sync and async execution: Mix blocking and non-blocking jobs freely
  • Type-safe responses: Access job outputs with full type safety
  • Conditional execution: Run jobs based on variables or previous responses
  • Built-in retry policies: Handle transient failures automatically
  • Testable: Each job is independently testable and workflow graph can be verified

You define jobs and how they connect and depend on each other, Chevere Workflow figures out the execution order and runs them accordingly.


# What does this solve for Laravel developers?

  • Dependency Management: Like Bus::chain(), execution order follows dependencies. But instead of manually sequencing every step, simply declare what data each job needs with response() and the engine infers the order.
  • Parallel Processing: Like Bus::batch(), independent jobs run concurrently using async().
  • Sequential Flows: Like Pipeline, you can model linear operations but workflow graphs also handle branching and fan-in without custom plumbing.
  • Conditional Execution: Like feature flags or guard clauses, jobs can be skipped using declarative withRunIf() and withRunIfNot().

# How This Integration Works

This Laravel package is a thin wrapper around chevere/workflow (opens new window). It provides:

Component What It Does
WorkflowServiceProvider Registers the workflow system with Laravel's service container
AbstractWorkflow Base class you extend to define workflows (like Laravel's Mailable or Notification)
WorkflowManager Service that runs workflows with Laravel's container for dependency injection
Workflow Facade Static access (Workflow::run(...)) for convenience
Artisan Commands make:workflow, workflow:run, workflow:list

This integration uses a PSR-11 bridge backed by Laravel's container, so your Laravel services (Eloquent models, mailers, cache, etc.) are automatically available for dependency injection inside workflow jobs.


# Installation

# Step 1: Install the package

composer require chevere/workflow-laravel

# Step 2: Verify installation

php artisan workflow:list

You should see: No workflows found.


# Quick Start: Your First Workflow

# Step 1: Generate a workflow

php artisan make:workflow GreetUser

This creates app/Workflows/GreetUser.php:

namespace App\Workflows;

use Chevere\Workflow\Interfaces\WorkflowInterface;
use Chevere\Workflow\Laravel\AbstractWorkflow;
use function Chevere\Workflow\sync;
use function Chevere\Workflow\variable;
use function Chevere\Workflow\workflow;

class GreetUser extends AbstractWorkflow
{
    protected function definition(): WorkflowInterface
    {
        return workflow(
            greet: sync(
                fn(string $name): string => "Hello, {$name}!",
                name: variable('name'),
            ),
        );
    }
}

# Step 2: Run it from a controller

namespace App\Http\Controllers;

use App\Workflows\GreetUser;
use Illuminate\Http\JsonResponse;

class GreetController extends Controller
{
    public function __invoke(GreetUser $workflow): JsonResponse
    {
        $run = $workflow->run(name: 'Taylor');

        return response()->json([
            'message' => $run->response('greet')->string(),
            // "Hello, Taylor!"
        ]);
    }
}

# Step 3: Or use the Facade

use Chevere\Workflow\Laravel\Facades\Workflow;
use App\Workflows\GreetUser;

$run = Workflow::run(GreetUser::class, name: 'Taylor');
echo $run->response('greet')->string(); // "Hello, Taylor!"

# Core Concepts

Chevere Workflow uses jobs, variables, and responses to build a dependency graph that automatically determines execution order. Jobs can be sync() or async(), variables are runtime inputs declared with variable(), and responses reference outputs from previous jobs using response().

For detailed information about these core concepts, execution graphs, and how the workflow engine works, see the Chevere Workflow documentation (opens new window).

Laravel-specific behavior: When you extend AbstractWorkflow, Laravel's service container is automatically available for dependency injection in your job callables.


# The AbstractWorkflow Class

This is the main class you extend. It follows the same pattern as Laravel's Mailable, Notification, or FormRequest:

namespace App\Workflows;

use Chevere\Workflow\Interfaces\WorkflowInterface;
use Chevere\Workflow\Laravel\AbstractWorkflow;
use function Chevere\Workflow\{workflow, sync, async, variable, response};

class ProcessOrder extends AbstractWorkflow
{
    protected function definition(): WorkflowInterface
    {
        return workflow(
            validate: sync(
                fn(int $orderId): array => ['id' => $orderId, 'valid' => true],
                orderId: variable('orderId'),
            ),
            charge: sync(
                fn(array $order): array => ['charged' => true, 'amount' => 99.99],
                order: response('validate'),
            ),
            sendReceipt: async(
                fn(array $charge): bool => true,
                charge: response('charge'),
            ),
            updateInventory: async(
                fn(array $order): bool => true,
                order: response('validate'),
            ),
        );
    }
}

# Methods Available

Method Returns Description
run(mixed ...$variables) RunInterface Execute the workflow
getWorkflow() WorkflowInterface Get the workflow definition (cached)
graph() array<array<string>> Get the execution graph

# Using in Controllers

Laravel auto-injects AbstractWorkflow subclasses via the container:

class OrderController extends Controller
{
    public function store(ProcessOrder $workflow, Request $request): JsonResponse
    {
        $run = $workflow->run(orderId: $request->input('order_id'));

        return response()->json([
            'charged' => $run->response('charge', 'charged')->bool(),
            'amount'  => $run->response('charge', 'amount')->float(),
        ]);
    }
}

# Using the Facade

The Workflow facade provides static access to the WorkflowManager:

use Chevere\Workflow\Laravel\Facades\Workflow;

// Run a workflow class
$run = Workflow::run(ProcessOrder::class, orderId: 42);

// Run an inline workflow definition
use function Chevere\Workflow\{workflow, sync, variable};

$run = Workflow::run(
    workflow(
        greet: sync(
            fn(string $name): string => "Hello, {$name}!",
            name: variable('name'),
        ),
    ),
    name: 'World'
);

# Using the WorkflowManager

Inject WorkflowManager when you prefer dependency injection over facades:

use Chevere\Workflow\Laravel\WorkflowManager;

class OrderService
{
    public function __construct(
        private WorkflowManager $workflows
    ) {}

    public function processOrder(int $orderId): array
    {
        $run = $this->workflows->run(ProcessOrder::class, orderId: $orderId);

        return $run->toArray();
    }
}

# Artisan Commands

# make:workflow

Generate a new workflow class:

php artisan make:workflow ProcessOrder
# Creates: app/Workflows/ProcessOrder.php

# workflow:run

Run a workflow from the command line:

php artisan workflow:run "App\Workflows\GreetUser" --var=name=Artisan

workflow:run accepts any class that extends Chevere\Workflow\Laravel\AbstractWorkflow.

Output:

Running workflow: App\Workflows\GreetUser
Execution graph:
  Level 0: greet
Workflow completed successfully.
UUID: a1b2c3d4-...
Responses:
  greet: Hello, Artisan!

Pass multiple variables:

php artisan workflow:run "App\Workflows\ProcessOrder" --var=orderId=42 --var=notify=true

# workflow:list

List all workflows found in the application:

php artisan workflow:list

Output:

+-----------------------------------------+--------+-------------+
| Workflow                                | Jobs   | Graph Depth |
+-----------------------------------------+--------+-------------+
| App\Workflows\ProcessOrder              | 4 jobs | 3 levels    |
| App\Workflows\ImageResize               | 4 jobs | 2 levels    |
+-----------------------------------------+--------+-------------+

Workflows are auto-discovered by scanning your application's app/ directory for classes extending AbstractWorkflow.


# Real-World Use Cases

# Use Case 1: User Registration Flow

This is the best starting point for Laravel developers. It mirrors what you'd normally build with events/listeners or job chains, but with automatic ordering.

namespace App\Workflows;

use App\Actions\CreateUser;
use App\Actions\SendWelcomeEmail;
use App\Actions\LogRegistration;
use App\Actions\AssignDefaultRole;
use Chevere\Workflow\Interfaces\WorkflowInterface;
use Chevere\Workflow\Laravel\AbstractWorkflow;
use function Chevere\Workflow\{workflow, sync, async, variable, response};

class RegisterUser extends AbstractWorkflow
{
    protected function definition(): WorkflowInterface
    {
        return workflow(
            // Step 1: Validate and create the user (must be sync — everything else depends on it)
            createUser: sync(
                CreateUser::class,
                email: variable('email'),
                password: variable('password'),
                name: variable('name'),
            ),

            // Step 2a: Send welcome email (async — can run async with 2b and 2c)
            sendWelcome: async(
                SendWelcomeEmail::class,
                userId: response('createUser', 'id'),
                email: response('createUser', 'email'),
            ),

            // Step 2b: Log the registration (async — runs async with 2a and 2c)
            logEvent: async(
                LogRegistration::class,
                userId: response('createUser', 'id'),
            ),

            // Step 2c: Assign default role (async — runs async with 2a and 2b)
            assignRole: async(
                AssignDefaultRole::class,
                userId: response('createUser', 'id'),
                role: 'member',
            ),
        );
    }
}

Execution graph:

Level 0: [createUser]                          ← runs first
Level 1: [sendWelcome, logEvent, assignRole]   ← run async after createUser

In your controller:

class RegisterController extends Controller
{
    public function store(Request $request, RegisterUser $workflow): JsonResponse
    {
        $request->validate([
            'email'    => 'required|email|unique:users',
            'password' => 'required|min:8',
            'name'     => 'required|string',
        ]);

        $run = $workflow->run(
            email: $request->input('email'),
            password: $request->input('password'),
            name: $request->input('name'),
        );

        return response()->json([
            'user_id' => $run->response('createUser', 'id')->int(),
            'message' => 'Registration complete',
        ], 201);
    }
}

What each Action class looks like (using chevere/action):

namespace App\Actions;

use App\Models\User;
use Chevere\Action\Action;
use Illuminate\Support\Facades\Hash;

class CreateUser extends Action
{
    public function __invoke(string $email, string $password, string $name): array
    {
        $user = User::create([
            'email'    => $email,
            'password' => Hash::make($password),
            'name'     => $name,
        ]);

        return ['id' => $user->id, 'email' => $user->email];
    }
}

You can also use plain closures instead of Action classes — see the Quick Start example.


# Use Case 2: Image Processing Pipeline

Process multiple image sizes async, then store them all:

namespace App\Workflows;

use Chevere\Workflow\Interfaces\WorkflowInterface;
use Chevere\Workflow\Laravel\AbstractWorkflow;
use Illuminate\Support\Facades\Storage;
use function Chevere\Workflow\{workflow, sync, async, variable, response};

class ProcessImage extends AbstractWorkflow
{
    protected function definition(): WorkflowInterface
    {
        return workflow(
            // These 3 resize jobs run async (no dependencies on each other)
            thumb: async(
                fn(string $path): string => $this->resize($path, 150, 150),
                path: variable('imagePath'),
            ),
            medium: async(
                fn(string $path): string => $this->resize($path, 800, 600),
                path: variable('imagePath'),
            ),
            large: async(
                fn(string $path): string => $this->resize($path, 1920, 1080),
                path: variable('imagePath'),
            ),

            // This runs after ALL three above complete
            store: sync(
                fn(string $thumb, string $medium, string $large): array => [
                    'thumb'  => Storage::put('thumbs', $thumb),
                    'medium' => Storage::put('medium', $medium),
                    'large'  => Storage::put('large', $large),
                ],
                thumb: response('thumb'),
                medium: response('medium'),
                large: response('large'),
            ),
        );
    }

    private function resize(string $path, int $w, int $h): string
    {
        // Your image processing logic (Intervention Image, GD, Imagick, etc.)
        return "/resized/{$w}x{$h}/" . basename($path);
    }
}

Execution graph:

Level 0: [thumb, medium, large]   ← async
Level 1: [store]                  ← after all three

# Use Case 3: Order Processing with Conditional Steps

This example demonstrates using withRunIf() for conditional job execution (learn more (opens new window)):

namespace App\Workflows;

use Chevere\Workflow\Interfaces\WorkflowInterface;
use Chevere\Workflow\Laravel\AbstractWorkflow;
use function Chevere\Workflow\{workflow, sync, async, variable, response};

class ProcessOrder extends AbstractWorkflow
{
    protected function definition(): WorkflowInterface
    {
        return workflow(
            validate: sync(
                fn(int $orderId): array => ['id' => $orderId, 'total' => 150.00, 'valid' => true],
                orderId: variable('orderId'),
            ),

            charge: sync(
                fn(array $order): array => [
                    'transactionId' => 'txn_' . $order['id'],
                    'amount' => $order['total'],
                ],
                order: response('validate'),
            ),

            // Conditionally send receipt based on runtime variable
            sendReceipt: async(
                fn(string $txnId, float $amount): bool => true,
                txnId: response('charge', 'transactionId'),
                amount: response('charge', 'amount'),
            )->withRunIf(
                variable('sendReceipt')
            ),
        );
    }
}

# Use Case 4: API Aggregation

Fetch data from multiple external APIs async:

namespace App\Workflows;

use Chevere\Workflow\Interfaces\WorkflowInterface;
use Chevere\Workflow\Laravel\AbstractWorkflow;
use Illuminate\Support\Facades\Http;
use function Chevere\Workflow\{workflow, sync, async, variable, response};

class FetchDashboardData extends AbstractWorkflow
{
    protected function definition(): WorkflowInterface
    {
        return workflow(
            // All three API calls run async
            weather: async(
                fn(string $city): array => Http::get("https://api.weather.example/v1/{$city}")->json(),
                city: variable('city'),
            ),
            news: async(
                fn(string $category): array => Http::get("https://api.news.example/v1/{$category}")->json(),
                category: variable('newsCategory'),
            ),
            stocks: async(
                fn(string $symbol): array => Http::get("https://api.stocks.example/v1/{$symbol}")->json(),
                symbol: variable('stockSymbol'),
            ),

            // Combine all results after they arrive
            dashboard: sync(
                fn(array $weather, array $news, array $stocks): array => [
                    'weather' => $weather,
                    'news' => $news,
                    'stocks' => $stocks,
                    'generated_at' => now()->toISOString(),
                ],
                weather: response('weather'),
                news: response('news'),
                stocks: response('stocks'),
            ),
        );
    }
}

Without Workflow, those 3 API calls would run sequentially (3x the latency). With async(), they run concurrently.


# Use Case 5: Data ETL Pipeline

Extract, transform, and load data with clear step separation:

namespace App\Workflows;

use Chevere\Workflow\Interfaces\WorkflowInterface;
use Chevere\Workflow\Laravel\AbstractWorkflow;
use function Chevere\Workflow\{workflow, sync, variable, response};

class ImportCsvData extends AbstractWorkflow
{
    protected function definition(): WorkflowInterface
    {
        return workflow(
            // Extract: read and parse CSV
            extract: sync(
                fn(string $path): array => array_map('str_getcsv', file($path)),
                path: variable('csvPath'),
            ),

            // Transform: clean and validate rows
            transform: sync(
                fn(array $rows): array => array_filter($rows, fn($row) => count($row) >= 3),
                rows: response('extract'),
            ),

            // Load: insert into database
            load: sync(
                fn(array $rows): int => count($rows), // DB::table('imports')->insert($rows)
                rows: response('transform'),
            ),
        );
    }
}

# Advanced Workflow Features

Chevere Workflow supports several advanced features:

  • Conditional Execution: Use withRunIf() and withRunIfNot() to conditionally skip jobs based on variables or previous job responses
  • Retry Policies: Configure automatic retries with withRetry() for transient failures
  • Sync/Async Execution: Use sync() for sequential jobs and async() for async execution
  • Mermaid Graphs: Visualize your workflow's execution graph with Mermaid syntax

For detailed documentation on these features, see the Chevere Workflow documentation (opens new window).


# Dependency Injection

This is where the Laravel integration shines. Laravel's container is automatically passed to Chevere Workflow, so your Action classes can use constructor injection:

use Chevere\Action\Action;
use App\Services\PaymentGateway;
use Illuminate\Log\LogManager;

class ChargePayment extends Action
{
    public function __construct(
        private PaymentGateway $gateway,  // ← auto-injected from Laravel
        private LogManager $logger,       // ← auto-injected from Laravel
    ) {}

    public function __invoke(int $orderId, float $amount): array
    {
        $this->logger->info("Charging order {$orderId}: \${$amount}");
        $result = $this->gateway->charge($amount);

        return ['transactionId' => $result->id, 'status' => $result->status];
    }
}

Any service registered in Laravel's container can be injected. No extra configuration needed.


# Error Handling

When a job fails, Chevere Workflow wraps the original exception in a WorkflowException:

use Chevere\Workflow\Exceptions\WorkflowException;

try {
    $run = $workflow->run(orderId: 42);
} catch (WorkflowException $e) {
    // Which job failed?
    echo $e->name;        // "charge"

    // The job instance
    $job = $e->job;

    // The original exception
    $original = $e->throwable;
    echo $original->getMessage();
}

In a Laravel controller:

public function store(Request $request, ProcessOrder $workflow): JsonResponse
{
    try {
        $run = $workflow->run(orderId: $request->input('order_id'));

        return response()->json($run->toArray());
    } catch (WorkflowException $e) {
        report($e->throwable); // Log with Laravel

        return response()->json([
            'error' => "Job '{$e->name}' failed: " . $e->throwable->getMessage(),
        ], 500);
    }
}

# Testing Your Workflows

# Setup

In a Laravel app consuming this package, use your application's base test case as usual:

namespace Tests\Feature;

use Tests\TestCase;

class ProcessOrderTest extends TestCase
{
    // ...
}

If you are developing this package itself (or another package), use Orchestra Testbench and register WorkflowServiceProvider:

use Chevere\Workflow\Laravel\WorkflowServiceProvider;
use Orchestra\Testbench\TestCase;

class ProcessOrderTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [WorkflowServiceProvider::class];
    }
}

# Test the Execution Graph

Verify jobs run in the expected order:

public function testGraphOrder(): void
{
    $workflow = $this->app->make(ProcessOrder::class);
    $graph = $workflow->graph();

    // Level 0: validate runs first
    $this->assertSame(['validate'], $graph[0]);

    // Level 1: charge runs after validate
    $this->assertSame(['charge'], $graph[1]);

    // Level 2: sendReceipt, updateInventory run async
    $this->assertContains('sendReceipt', $graph[2]);
    $this->assertContains('updateInventory', $graph[2]);
}

# Test Responses

public function testWorkflowResponses(): void
{
    $workflow = $this->app->make(ProcessOrder::class);
    $run = $workflow->run(orderId: 42, sendReceipt: true, hasDiscount: false);

    $this->assertTrue($run->response('charge', 'charged')->bool());
    $this->assertSame(42, $run->response('validate', 'id')->int());
}

# Test Conditional Execution

public function testSkippedJobs(): void
{
    $workflow = $this->app->make(ProcessOrder::class);
    $run = $workflow->run(orderId: 42, sendReceipt: false, hasDiscount: false);

    $this->assertTrue($run->skip()->contains('sendReceipt'));
    $this->assertTrue($run->skip()->contains('applyDiscount'));
}

# Test Error Handling

use Chevere\Workflow\Exceptions\WorkflowException;

public function testJobFailure(): void
{
    $this->expectException(WorkflowException::class);

    $workflow = $this->app->make(FailingWorkflow::class);
    $workflow->run(input: 'invalid');
}

# Test with the Facade

use Chevere\Workflow\Laravel\Facades\Workflow;

public function testFacade(): void
{
    $run = Workflow::run(ProcessOrder::class, orderId: 42);

    $this->assertNotEmpty($run->uuid());
}

# FAQ

# How is this different from Laravel Queues?

Laravel Queues push jobs to a queue worker (Redis, SQS, database) for background processing. Chevere Workflow runs jobs in the current process (with AMP async for concurrency). Use queues when you need background processing; use workflows when you need coordinated multi-step logic with data flowing between steps. By the way, you can dispatch a queued job that internally runs a workflow!

# Can I use Eloquent models inside jobs?

Yes. Laravel's container is passed to the workflow engine, so any service registered in the container (including Eloquent, facades, etc.) works normally.

# Can I mix this with Laravel's queue system?

Yes. You can dispatch a queued job that internally runs a workflow. The workflow itself runs synchronously/async within the queue worker process.

class ProcessOrderJob implements ShouldQueue
{
    public function handle(ProcessOrder $workflow): void
    {
        $workflow->run(orderId: $this->orderId);
    }
}

# Does the Facade work in Tinker?

Yes:

>>> Chevere\Workflow\Laravel\Facades\Workflow::run(App\Workflows\GreetUser::class, name: 'Tinker')