# 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 withresponse()and the engine infers the order. - Parallel Processing: Like
Bus::batch(), independent jobs run concurrently usingasync(). - 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()andwithRunIfNot().
# 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()andwithRunIfNot()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 andasync()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')