Chevere Workflow: A Declarative PHP Workflow Engine with Async Job Execution
Published on by Yannick Lyn Fatt
When your application needs to run a sequence of steps — some dependent on each other, others that could run at the same time — it's tempting to stuff everything into a single class or chain method calls together. The Chevere Workflow package offers a cleaner approach: declare your jobs, wire up their dependencies, and let the engine automatically determine execution order.
Installation
composer require chevere/workflow
A Practical Example
Consider a user registration flow that needs to:
- Create the user account
- Send a welcome email
- Set up a free trial subscription
- Notify your team on Slack
Some of these steps depend on the account being created first, while others can run in parallel once it is. Managing this manually means tracking shared state, writing glue code, and handling partial failures yourself. Workflow handles all of that declaratively.
Building the Pipeline
Here is how you might model the onboarding flow above:
use function Chevere\Workflow\{workflow, sync, async, variable, response, run}; $workflow = workflow( createAccount: sync( CreateUserAccount::class, name: variable('name'), email: variable('email'), password: variable('password'), ), sendWelcomeEmail: async( SendWelcomeEmail::class, userId: response('createAccount', 'id'), email: response('createAccount', 'email'), ), createTrial: sync( CreateTrialSubscription::class, userId: response('createAccount', 'id'), )->withRunIf(variable('enableTrial')), notifyTeam: async( NotifySlackChannel::class, userEmail: response('createAccount', 'email'), ),);
A few things worth noting here:
sync()blocks until the job completes;async()runs concurrently once its dependencies are metvariable('name')is a placeholder for a value you supply at runtimeresponse('createAccount', 'id')references theidfield fromcreateAccount's output->withRunIf(variable('enableTrial'))makes thecreateTrialjob conditional — it is skipped entirely ifenableTrialis falsy
The engine builds a dependency graph from these declarations. Because sendWelcomeEmail and notifyTeam both depend only on createAccount, they run concurrently once the account is ready — without you needing to orchestrate that yourself.
Defining a Job
Each job can be a closure, an invocable class, or a class extending Chevere's Action. Here's a straightforward invocable:
use Chevere\Action\Action; class CreateUserAccount extends Action{ public function __invoke( string $name, string $email, string $password, ): array { $user = User::create([ 'name' => $name, 'email' => $email, 'password' => bcrypt($password), ]); return [ 'id' => $user->id, 'email' => $user->email, ]; }}
The return value becomes the job's response. Downstream jobs reference individual fields via response('createAccount', 'id') or the whole response via response('createAccount').
Running the Workflow
Pass your variables directly to run(), and access any job's output from the result:
$result = run( $workflow, name: 'Jane Doe', email: 'jane@example.com', password: 'secret', enableTrial: true,); $userId = $result->response('createAccount')->int('id');
Handling Failures
If any job throws, Workflow wraps it in a WorkflowException that tells you exactly which job failed and why:
use Chevere\Workflow\Exceptions\WorkflowException; try { $result = run($workflow, ...);} catch (WorkflowException $e) { logger()->error("Job '{$e->name}' failed", [ 'error' => $e->throwable->getMessage(), ]);}
Retrying Unreliable Jobs
For jobs that call external services prone to occasional hiccups, attach a retry policy with withRetry():
notifyTeam: async( NotifySlackChannel::class, userEmail: response('createAccount', 'email'),)->withRetry(timeout: 60, maxAttempts: 3, delay: 5),
Laravel Integration
A dedicated package brings first-class Laravel support, adding Artisan commands, an AbstractWorkflow base class, and a facade for running workflows anywhere in your app.
composer require chevere/workflow-laravel
Generate a workflow class with Artisan:
php artisan make:workflow UserOnboarding
Then fill in the definition using the same jobs from earlier:
use Chevere\WorkflowLaravel\AbstractWorkflow;use Chevere\Workflow\Interfaces\WorkflowInterface;use function Chevere\Workflow\{workflow, sync, async, variable, response}; class UserOnboarding extends AbstractWorkflow{ protected function definition(): WorkflowInterface { return workflow( createAccount: sync( CreateUserAccount::class, name: variable('name'), email: variable('email'), password: variable('password'), ), sendWelcomeEmail: async( SendWelcomeEmail::class, userId: response('createAccount', 'id'), email: response('createAccount', 'email'), ), createTrial: sync( CreateTrialSubscription::class, userId: response('createAccount', 'id'), )->withRunIf(variable('enableTrial')), notifyTeam: async( NotifySlackChannel::class, userEmail: response('createAccount', 'email'), ), ); }}
Run it via the Workflow facade — from a controller, a job, or anywhere else:
use Chevere\WorkflowLaravel\Facades\Workflow; $result = Workflow::run(UserOnboarding::class, name: $request->name, email: $request->email, password: $request->password, enableTrial: $request->boolean('enable_trial'),); $userId = $result->response('createAccount')->int('id');
Laravel's service container automatically handles constructor injection for class-based jobs, so any service your job needs can be type-hinted and resolved without extra configuration.
You can also list all registered workflows with php artisan workflow:list, or trigger one directly from the terminal with php artisan workflow:run.
Visualising the Graph
If you use VS Code, the package has an official extension that renders your workflow as a Mermaid flowchart. The diagram shows each job as a node, draws edges between dependent jobs, and annotates those edges with the data flowing between them — so you can see at a glance that sendWelcomeEmail waits on createAccount, or that createTrial only runs when enableTrial is truthy. Because the graph is derived directly from your code, it stays accurate as you add or rearrange jobs with nothing to maintain separately.
Wrapping Up
Chevere Workflow provides structure for multi-step processes that would otherwise scatter logic across multiple classes or make a single method unwieldy. The declarative style makes it easy to see what runs when, and the automatic dependency graph means parallel execution happens with no extra configuration.
The package also ships with testing utilities so you can verify each job in isolation and assert that your workflow's execution graph matches your expectations — something that is much harder when steps are tightly coupled.
Check out the Chevere Workflow repository on GitHub for the full feature set, including Action classes, Workflow Providers, and the VS Code extension.