PHPackages                             chevere/workflow - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. chevere/workflow

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

chevere/workflow
================

Declarative workflow engine for PHP with automatic dependency resolution, sync/async job execution, and type-safe response chaining.

3.0.10(1mo ago)1111.8k↑50%2[4 issues](https://github.com/chevere/workflow/issues)1Apache-2.0PHPPHP ^8.1CI passing

Since Feb 22Pushed 2mo ago3 watchersCompare

[ Source](https://github.com/chevere/workflow)[ Packagist](https://packagist.org/packages/chevere/workflow)[ Docs](https://chevere.org)[ RSS](/packages/chevere-workflow/feed)WikiDiscussions 3.0 Synced 1mo ago

READMEChangelog (10)Dependencies (21)Versions (45)Used By (1)

Workflow
========

[](#workflow)

[![Chevere](chevere.svg)](chevere.svg)

[![Build](https://camo.githubusercontent.com/0a435396a22aa6911ae926791eae3dc94bc65d639278df6ea3f6b8c8eeea75fd/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f636865766572652f776f726b666c6f772f746573742e796d6c3f6272616e63683d322e31267374796c653d666c61742d737175617265)](https://github.com/chevere/workflow/actions)[![Code size](https://camo.githubusercontent.com/761956ebd1a5100c89058f31b45724deb0857684b5077cbe048d0e3ec1a91609/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c616e6775616765732f636f64652d73697a652f636865766572652f776f726b666c6f773f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/761956ebd1a5100c89058f31b45724deb0857684b5077cbe048d0e3ec1a91609/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c616e6775616765732f636f64652d73697a652f636865766572652f776f726b666c6f773f7374796c653d666c61742d737175617265)[![Apache-2.0](https://camo.githubusercontent.com/acab821aeb9671d19300c9e571bfd141a986d74036a069d3c50c38210dbcffb2/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f636865766572652f776f726b666c6f773f7374796c653d666c61742d737175617265)](LICENSE)[![PHPStan](https://camo.githubusercontent.com/6016298b28550819030c76e9327f62501596a31fd76406695bae2f3d2a1f26a4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230392d626c756576696f6c65743f7374796c653d666c61742d737175617265)](https://phpstan.org/)[![Mutation testing badge](https://camo.githubusercontent.com/1f19c1b530119ed4028ff07a3b7c8ce222fdb8b3f8a7d9bb85324d7be5d36516/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f7374796c653d666c61742d7371756172652675726c3d687474707325334125324625324662616467652d6170692e737472796b65722d6d757461746f722e696f2532466769746875622e636f6d25324663686576657265253246776f726b666c6f77253246322e31)](https://dashboard.stryker-mutator.io/reports/github.com/chevere/workflow/2.1)

[![Quality Gate Status](https://camo.githubusercontent.com/c2a13a1729471aecb66cc8dc9fbd675d47764f252a9b1bc62734225e8b4c0ae9/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d636865766572655f776f726b666c6f77266d65747269633d616c6572745f737461747573)](https://sonarcloud.io/dashboard?id=chevere_workflow)[![Maintainability Rating](https://camo.githubusercontent.com/eef2ba8f0f8132e971baa4767bc6d8afcb699e08630cc702ba654f15155fe64b/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d636865766572655f776f726b666c6f77266d65747269633d7371616c655f726174696e67)](https://sonarcloud.io/dashboard?id=chevere_workflow)[![Reliability Rating](https://camo.githubusercontent.com/814edd07f279697f49c12a598b4039b17adcdf3ac71d8c6ae2e39d75237bffed/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d636865766572655f776f726b666c6f77266d65747269633d72656c696162696c6974795f726174696e67)](https://sonarcloud.io/dashboard?id=chevere_workflow)[![Security Rating](https://camo.githubusercontent.com/fcb8ca3483ac5814974ea4a5be983814f2bd71f266eff12e113b3cb673dccadf/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d636865766572655f776f726b666c6f77266d65747269633d73656375726974795f726174696e67)](https://sonarcloud.io/dashboard?id=chevere_workflow)[![Coverage](https://camo.githubusercontent.com/59f98cddb7d9833caa6929b996387ab87c8e914f01ac50dcba476e0c83e79894/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d636865766572655f776f726b666c6f77266d65747269633d636f766572616765)](https://sonarcloud.io/dashboard?id=chevere_workflow)[![Technical Debt](https://camo.githubusercontent.com/ec9a61902c1029b5491226ee9ff2dc0add5b2d9fa4703a7f66440c2a47ccd21e/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d636865766572655f776f726b666c6f77266d65747269633d7371616c655f696e646578)](https://sonarcloud.io/dashboard?id=chevere_workflow)[![CodeFactor](https://camo.githubusercontent.com/e3e1d5bb8f810a23eaa44ee6535e6d311504a4f4193d613c75d3c3d39f3c8f45/68747470733a2f2f7777772e636f6465666163746f722e696f2f7265706f7369746f72792f6769746875622f636865766572652f776f726b666c6f772f6261646765)](https://www.codefactor.io/repository/github/chevere/workflow)

Summary
-------

[](#summary)

**Chevere Workflow** 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.

Installing
----------

[](#installing)

Workflow is available through [Packagist](https://packagist.org/packages/chevere/workflow) and the repository source is at [chevere/workflow](https://github.com/chevere/workflow).

```
composer require chevere/workflow
```

Quick Start
-----------

[](#quick-start)

Here's a minimal example to get you started:

```
use function Chevere\Workflow\{workflow, sync, variable, run};

// 1. Define a workflow with jobs
$workflow = workflow(
    greet: sync(
        fn(string $name): string => "Hello, {$name}!",
        name: variable('username')
    )
);

// 2. Run with variables
$result = run($workflow, username: 'World');

// 3. Get typed responses
echo $result->response('greet')->string();
// Output: Hello, World!
```

Core Concepts
-------------

[](#core-concepts)

Workflow is built around four main concepts:

ConceptDescription**Job**A unit of work that produces a response**Variable**External input provided when running the workflow**Response**Reference to output from a previous job**Graph**Automatic execution order based on job dependencies### How It Works

[](#how-it-works)

1. You define jobs using `sync()` or `async()` functions
2. Jobs declare their inputs: literal values, `variable()` references, or `response()` from other jobs
3. The engine builds a dependency graph automatically
4. Jobs execute in optimal order (parallel when possible)
5. Access typed responses after execution

Functions Reference
-------------------

[](#functions-reference)

FunctionPurpose`workflow()`Create a workflow from named jobs`sync()`Create a synchronous (blocking) job`async()`Create an asynchronous (non-blocking) job`variable()`Declare a runtime variable`response()`Reference another job's output`run()`Execute a workflow with variables---

Jobs
----

[](#jobs)

Jobs are the building blocks of a workflow. Each job wraps an executable unit ([Action](https://chevere.org/packages/action), [Closure](https://www.php.net/manual/en/class.closure.php), Invocable class, or any PHP [callable](https://www.php.net/manual/en/language.types.callable.php)) and declares its input arguments.

### Creating Jobs with Closures

[](#creating-jobs-with-closures)

Use closures for simple, inline operations:

```
use function Chevere\Workflow\{workflow, sync, async, variable, response, run};

$workflow = workflow(
    // Simple calculation
    add: sync(
        fn(int $a, int $b): int => $a + $b,
        a: 10,
        b: variable('number')
    ),
    // Format the result
    format: sync(
        fn(int $sum): string => "Sum: {$sum}",
        sum: response('add')
    )
);

$result = run($workflow, number: 5);
echo $result->response('format')->string(); // Sum: 15
```

### Creating Jobs with Action Classes

[](#creating-jobs-with-action-classes)

For complex or reusable logic, use [Action](https://chevere.org/packages/action) classes as these additionally support method definitions for `acceptParameters()` and `acceptReturn()` to define parameter and return rules that are automatically applied at runtime.

```
use Chevere\Action\Action;

class FetchUser extends Action
{
    public function __invoke(int $userId): array
    {
        // Fetch user from database
        return ['id' => $userId, 'name' => 'John', 'email' => 'john@example.com'];
    }
}

class SendEmail extends Action
{
    public function __invoke(string $email, string $subject): bool
    {
        // Send email logic
        return true;
    }
}
```

```
$workflow = workflow(
    user: sync(
        FetchUser::class,
        userId: variable('id')
    ),
    notify: sync(
        SendEmail::class,
        email: response('user', 'email'),
        subject: 'Welcome!'
    )
);

$result = run($workflow, id: 123);
```

### Creating Jobs with Invocable Classes

[](#creating-jobs-with-invocable-classes)

Use invocable classes (classes with `__invoke` method) for reusable logic without needing Action base class:

```
class CalculateTotal
{
    public function __invoke(array $items, float $taxRate): float
    {
        $subtotal = array_sum(array_column($items, 'price'));
        return $subtotal * (1 + $taxRate);
    }
}

class FormatCurrency
{
    public function __invoke(float $amount, string $currency = 'USD'): string
    {
        return $currency . ' ' . number_format($amount, 2);
    }
}
```

```
$workflow = workflow(
    total: sync(
        CalculateTotal::class,
        items: variable('items'),
        taxRate: 0.08
    ),
    formatted: sync(
        FormatCurrency::class,
        amount: response('total'),
        currency: 'EUR'
    )
);

$result = run($workflow, items: [
    ['name' => 'Item 1', 'price' => 10.00],
    ['name' => 'Item 2', 'price' => 20.00]
]);
echo $result->response('formatted')->string(); // EUR 32.40
```

### Creating Jobs with Callables

[](#creating-jobs-with-callables)

Use any PHP callable including array callbacks, function names, or static methods:

```
class StringHelper
{
    public static function uppercase(string $text): string
    {
        return strtoupper($text);
    }

    public function reverse(string $text): string
    {
        return strrev($text);
    }
}
```

```
$helper = new StringHelper();

$workflow = workflow(
    // Using built-in PHP function
    trim: sync(
        'trim',
        string: variable('input')
    ),
    // Using static method
    upper: sync(
        [StringHelper::class, 'uppercase'],
        text: response('trim')
    ),
    // Using instance method
    reversed: sync(
        [$helper, 'reverse'],
        text: response('upper')
    )
);

$result = run($workflow, input: '  hello  ');
echo $result->response('reversed')->string(); // OLLEH
```

### Sync vs Async Jobs

[](#sync-vs-async-jobs)

**Synchronous jobs** (`sync`) block execution until complete. Use for operations that must run in sequence:

```
workflow(
    first: sync(ActionA::class),  // Runs first
    second: sync(ActionB::class), // Waits for first
    third: sync(ActionC::class),  // Waits for second
);
// Graph: first → second → third
```

**Asynchronous jobs** (`async`) run concurrently when they have no dependencies:

```
workflow(
    resize1: async(ResizeImage::class, size: 'thumb'),
    resize2: async(ResizeImage::class, size: 'medium'),
    resize3: async(ResizeImage::class, size: 'large'),
    store: sync(StoreFiles::class, files: response('resize1'), ...)
);
// Graph: [resize1, resize2, resize3] → store
```

### Job Arguments

[](#job-arguments)

Jobs accept three types of arguments:

```
workflow(
    example: sync(
        MyAction::class,
        literal: 'fixed value',           // Literal value
        dynamic: variable('userInput'),   // Runtime variable
        chained: response('otherJob'),    // Previous job output
    )
);
```

Jobs can define I/O rules via [chevere/parameter](https://chevere.org/packages/parameter). Workflow derives parameter and return definitions from the callable signature or Action reflection and validates inputs and responses at runtime.

### Integration with Chevere

[](#integration-with-chevere)

Workflow works seamlessly with the `chevere/parameter` and `chevere/action` packages. When you declare parameter rules with `chevere/parameter` (types, ranges, or custom validators), those rules travel with the job definitions and are applied automatically at runtime. Workflow performs the validation layer for you before invoking jobs so callers don't need to repeat input or response checks. The same integration applies to `chevere/action` Action classes: parameter and return definitions are derived from action signatures and validated by Workflow.

These integrations are optional extras. You do not have to use `chevere/parameter` or `chevere/action` to use Workflow, but opting in gives stronger guarantees and reduces validation boilerplate across jobs.

---

Variables
---------

[](#variables)

Variables are placeholders for values provided at runtime. Declare them with `variable()`:

```
$workflow = workflow(
    job1: sync(
        SomeAction::class,
        name: variable('userName'),
        age: variable('userAge')
    )
);

// Provide values when running
$result = run($workflow, userName: 'Alice', userAge: 30);
```

All declared variables must be provided when running the workflow.

---

Responses
---------

[](#responses)

Use `response()` to pass output from one job to another. This automatically establishes a dependency.

```
$workflow = workflow(
    fetch: sync(
        FetchData::class,
        url: variable('endpoint')
    ),
    process: sync(
        ProcessData::class,
        data: response('fetch')  // Gets entire response from 'fetch'
    ),
    extract: sync(
        ExtractField::class,
        value: response('fetch', 'items')  // Gets 'items' key from response
    )
);
```

### Accessing Nested Response Keys/Properties

[](#accessing-nested-response-keysproperties)

When a job returns `array` or `object`, access specific keys/properties directly in `response()`:

```
response('user')           // job:user       Entire response object
response('user', 'id')     // job:user->id   id property from object response
response('post', 'id')     // job:post['id'] id key from array response
```

---

Execution Graph
---------------

[](#execution-graph)

The workflow engine automatically builds an execution graph based on job dependencies. Jobs without dependencies run in parallel (when using `async`), while dependent jobs wait for their dependencies.

```
$workflow = workflow(
    // Independent async jobs run in parallel
    thumb: async(ImageResize::class, size: 'thumb', file: variable('image')),
    medium: async(ImageResize::class, size: 'medium', file: variable('image')),
    large: async(ImageResize::class, size: 'large', file: variable('image')),
    // Sync job waits for all above
    store: sync(
        StoreFiles::class,
        thumb: response('thumb'),
        medium: response('medium'),
        large: response('large')
    )
);
$graph = $workflow->jobs()->graph()->toArray();
// [
//     ['thumb', 'medium', 'large'],  // Level 0: parallel
//     ['store']                      // Level 1: after dependencies
// ]
```

 ```
graph TD
    thumb --> store
    medium --> store
    large --> store
```

      Loading ---

Mermaid Graphs
--------------

[](#mermaid-graphs)

Workflow's graph can be rendered as a Mermaid flowchart for visualization. Each job is a node, and edges represent dependencies. Job conditions are annotated on the node labels.

Generate a Mermaid flowchart using `Mermaid::generate()`:

```
$workflow = workflow(
    ja: async(
        fn (): int => 1
    ),
    jb: async(
        fn (): int => 2
    )
        ->withRunIf(response('ja'))
        ->withRunIfNot(variable('var')),
    j1: async(
        #[_return(new _arrayp(
            id: new _int(),
            name: new _string()
        ))]
        fn (): array => [
            'id' => 123,
            'name' => 'example',
        ]
    ),
    j2: sync(
        fn (int $n, string $m): int => $n + $m,
        n: response('j1', 'id'),
        m: response('j1', 'name')
    ),
    j3: sync(
        fn (int $a): int => $a,
        a: response('jb')
    ),
    j4: sync(
        fn (int $i, int $j): int => $i * $j,
        i: response('j2'),
        j: response('j3')
    ),
);
$mermaid = Mermaid::generate($workflow);
```

 ```
graph TB;
    ja("`ja`");
    j1("`j1`");
    j2("`j2`");
    jb("`jb
*if* res(ja)
*ifNot* var(var)`");
    j3("`j3`");
    j4("`j4`");

    j1-->|"j1->id @ j2(n:)
j1->name @ j2(m:)"|j2;
    ja-->jb;
    jb-->|"jb @ j3(a:)"|j3;
    j2-->|"j2 @ j4(i:)"|j4;
    j3-->|"j3 @ j4(j:)"|j4;
```

      Loading Where:

- ***if* res(ja)**Job `jb` runs only if job `ja` response is truthy
- ***ifNot* var(var) 1 true**Job `jb` runs only if `var` variable is not equal to `1` or `true`
- **j1-&gt;id @ j2(n:)**Job `j1` response key/property `id` is used as argument `n` for job `j2`
- **j1-&gt;name @ j2(m:)**Job `j1` response key/property `name` is used as argument `m` for job `j2`
- **jb @ j3(a:)**Job `jb` response is used as argument `a` for job `j3`
- **j2 @ j4(i:)**Job `j2` response is used as argument `i` for job `j4`
- **j3 @ j4(j:)**Job `j3` response is used as argument `j` for job `j4`

---

Running Workflows
-----------------

[](#running-workflows)

Execute a workflow with the `run()` function:

```
use function Chevere\Workflow\run;

// Basic execution
$result = run($workflow, var1: 'value1', var2: 'value2');

// With dependency injection container
$result = run($workflow, $container, var1: 'value1');
```

### Accessing Responses

[](#accessing-responses)

The run result provides type-safe access to job responses:

```
$result = run($workflow, ...);

// Get typed responses
$result->response('jobName')->string();     // string
$result->response('jobName')->int();        // int
$result->response('jobName')->float();      // float
$result->response('jobName')->bool();       // bool
$result->response('jobName')->array();      // array

// Access array keys directly
$result->response('jobName', 'key')->string();
$result->response('jobName', 'nested', 'key')->int();
```

### Check Skipped Jobs

[](#check-skipped-jobs)

When using conditional execution, check which jobs were skipped:

```
if ($result->skip()->contains('optionalJob')) {
    // Job was skipped
}
```

---

Dependency Injection
--------------------

[](#dependency-injection)

Workflow supports automatic dependency injection for any class passed as a class-string using any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container. When your jobs reference classes with constructor dependencies, you can provide a container that will automatically resolve and inject those dependencies. [chevere/container](https://chevere.org/packages/container) is one example, but any PSR-11 container works.

### Passing a Container

[](#passing-a-container)

Pass a `ContainerInterface` instance as the second argument to `run()`:

```
use Chevere\Container\Container; // or any PSR-11 container
use function Chevere\Workflow\run;

// Create container with dependencies
$container = new Container(
    logger: new Logger(),
    database: new Database()
);

// Run workflow with container
$result = run($workflow, $container, ...$vars);
```

When a job references a class-string (Action class, invokable class, or any other class), Workflow uses the container to:

1. **Auto-inject dependencies** - Automatically resolve constructor parameters from the container
2. **Validate availability** - Ensure all required dependencies are present before execution
3. **Support nested dependencies** - Recursively resolve dependencies of dependencies

**Note:** Dependency injection only works for classes passed as class-strings (e.g., `MyClass::class`). It does not work for closures, already instantiated objects, or array callbacks.

### Example with Action Dependencies

[](#example-with-action-dependencies)

```
use Chevere\Action\Action;

class SendNotification extends Action
{
    // Dependencies injected automatically
    public function __construct(
        private LoggerInterface $logger,
        private MailerInterface $mailer
    ) {}

    public function __invoke(string $email, string $message): bool
    {
        $this->logger->info("Sending email to {$email}");
        return $this->mailer->send($email, $message);
    }
}

// Provide dependencies in container
$container = new Container(
    logger: new ConsoleLogger(),
    mailer: new SmtpMailer()
);

$workflow = workflow(
    notify: sync(
        SendNotification::class,  // Dependencies auto-injected
        email: variable('userEmail'),
        message: 'Welcome!'
    )
);

$result = run($workflow, $container, userEmail: 'user@example.com');
```

### Example with Invokable Class Dependencies

[](#example-with-invokable-class-dependencies)

Dependency injection also works with invokable classes and any other class:

```
class ProcessOrder
{
    // Dependencies injected automatically
    public function __construct(
        private DatabaseInterface $database,
        private PaymentGateway $payment
    ) {}

    public function __invoke(int $orderId, float $amount): array
    {
        $order = $this->database->getOrder($orderId);
        $result = $this->payment->charge($amount);
        return ['order' => $order, 'payment' => $result];
    }
}

// Provide dependencies in container
$container = new Container(
    database: new MySQLDatabase(),
    payment: new StripeGateway()
);

$workflow = workflow(
    process: sync(
        ProcessOrder::class,  // Dependencies auto-injected
        orderId: variable('orderId'),
        amount: variable('amount')
    )
);

$result = run($workflow, $container, orderId: 123, amount: 99.99);
```

---

Conditional Execution
---------------------

[](#conditional-execution)

Control whether a job runs using `withRunIf()` (run when conditions are met) or `withRunIfNot()` (skip when conditions are met). Both methods accept the same kinds of conditions and are evaluated at run-time.

### Accepted condition types

[](#accepted-condition-types)

- `boolean` literal — evaluated directly
- `variable('name')` — runtime argument coerced to boolean
- `response('job')` or `response('job', 'key')` — uses another job's output
- `callable` — invokes a callable passing the current `RunInterface` context argument

---

```
use function Chevere\Workflow\{workflow, sync, variable, run, response};

$workflow = workflow(
    isTooBig: sync(
        fn(string $path, int $maxBytes): bool => filesize($path) > $maxBytes,
        path: variable('file'),
        maxBytes: variable('maxBytes')
    ),
    compress: sync(
        CompressImage::class,
        file: variable('file')
    )->withRunIf(
        true,                       // literal
        variable('shouldCompress'), // workflow variable
        response('isTooBig'),       // job response value
        fn(RunInterface $run) => $run->variable('shouldCompress')->bool(), // closure condition variable
        fn(RunInterface $run) => $run->response('isTooBig')->bool(), // closure condition using response
    )
);
$result = run($workflow,
    file: '/path/to/image.jpg',
    shouldCompress: true,
    maxBytes: 1_000_000
);
```

---

Explicit Dependencies
---------------------

[](#explicit-dependencies)

While `response()` creates implicit dependencies, use `withDepends()` for explicit control:

```
$workflow = workflow(
    setup: async(SetupAction::class),
    process: async(
        ProcessAction::class,
        data: variable('input')
    )->withDepends('setup')  // Wait for setup even without using its response
);
```

---

Retry Policy
------------

[](#retry-policy)

Configure automatic retries for jobs that may fail transiently:

```
$workflow = workflow(
    fetch: sync(
        FetchFromApi::class,
        url: variable('endpoint')
    )->withRetry(
        timeout: 300,     // Max 300 seconds total
        maxAttempts: 5,   // Try up to 5 times
        delay: 10         // Wait 10 seconds between attempts
    )
);
```

ParameterTypeDefaultDescription`timeout``int``0`Max execution time in seconds (0 = unlimited)`maxAttempts``int``1`Total attempts including initial`delay``int``0`Seconds between retries (0 = immediate)Retry delays use non-blocking sleep, making them safe for async runtimes.

---

Exception Handling
------------------

[](#exception-handling)

When a job fails, a `WorkflowException` wraps the original exception:

```
use Chevere\Workflow\Exceptions\WorkflowException;

try {
    $result = run($workflow, ...);
} catch (WorkflowException $e) {
    echo $e->name;        // Name of the failed job
    echo $e->job;         // Job instance
    echo $e->throwable;   // Original exception
}
```

---

Using WorkflowTrait
-------------------

[](#using-workflowtrait)

For class-based workflow management, use `WorkflowTrait`:

```
use Chevere\Workflow\Traits\WorkflowTrait;
use function Chevere\Workflow\{workflow, sync, variable};

class OrderProcessor
{
    use WorkflowTrait;

    public function process(int $orderId): void
    {
        $workflow = workflow(
            validate: sync(ValidateOrder::class, id: variable('orderId')),
            charge: sync(ChargePayment::class, order: response('validate')),
            fulfill: sync(FulfillOrder::class, order: response('charge'))
        );

        $this->execute($workflow, orderId: $orderId);
    }

    public function getResult(): string
    {
        return $this->run()->response('fulfill')->string();
    }
}
```

---

Testing
-------

[](#testing)

### Testing Actions

[](#testing-actions)

Test your Action classes independently:

```
use PHPUnit\Framework\TestCase;

class FetchUserTest extends TestCase
{
    public function testFetchUser(): void
    {
        $action = new FetchUser();
        $result = $action(userId: 123);

        $this->assertSame(123, $result['id']);
        $this->assertArrayHasKey('name', $result);
    }
}
```

### Testing Workflow Graph

[](#testing-workflow-graph)

Verify execution order:

```
public function testWorkflowGraph(): void
{
    $workflow = workflow(
        a: async(ActionA::class),
        b: async(ActionB::class),
        c: sync(ActionC::class, x: response('a'), y: response('b'))
    );

    $graph = $workflow->jobs()->graph()->toArray();

    $this->assertSame([['a', 'b'], ['c']], $graph);
}
```

### Testing Responses

[](#testing-responses)

Test complete workflow execution:

```
public function testWorkflowResponses(): void
{
    $result = run($workflow, input: 'test');

    $this->assertSame('expected', $result->response('job1')->string());
    $this->assertSame(42, $result->response('job2', 'count')->int());
}
```

### Testing Exceptions

[](#testing-exceptions)

Use `ExpectWorkflowExceptionTrait` for error scenarios:

```
use Chevere\Workflow\Traits\ExpectWorkflowExceptionTrait;

class WorkflowExceptionTest extends TestCase
{
    use ExpectWorkflowExceptionTrait;

    public function testJobFailure(): void
    {
        $this->expectWorkflowException(
            closure: fn() => run($workflow, input: 'invalid'),
            exception: InvalidArgumentException::class,
            job: 'validate',
            message: 'Invalid input provided'
        );
    }
}
```

---

Real-World Examples
-------------------

[](#real-world-examples)

### Image Processing Pipeline

[](#image-processing-pipeline)

```
$workflow = workflow(
    // Parallel image resizing
    thumb: async(
        ImageResize::class,
        file: variable('image'),
        width: 150,
        height: 150
    ),
    medium: async(
        ImageResize::class,
        file: variable('image'),
        width: 800
    ),
    // Store after all resizing completes
    store: sync(
        StoreFiles::class,
        thumb: response('thumb'),
        medium: response('medium'),
        directory: variable('outputDir')
    )
);

$result = run($workflow,
    image: '/uploads/photo.jpg',
    outputDir: '/processed/'
);
```

### User Registration Flow

[](#user-registration-flow)

```
$workflow = workflow(
    validate: sync(
        ValidateRegistration::class,
        email: variable('email'),
        password: variable('password')
    ),
    createUser: sync(
        CreateUser::class,
        data: response('validate')
    ),
    sendWelcome: async(
        SendWelcomeEmail::class,
        user: response('createUser')
    ),
    logEvent: async(
        LogRegistration::class,
        userId: response('createUser', 'id')
    )
);
```

### Conditional Processing

[](#conditional-processing)

```
$workflow = workflow(
    analyze: sync(
        AnalyzeContent::class,
        content: variable('text')
    ),
    translate: sync(
        TranslateContent::class,
        text: variable('text'),
        targetLang: variable('lang')
    )->withRunIf(
        variable('needsTranslation')
    ),
    publish: sync(
        PublishContent::class,
        content: response('analyze'),
        translated: response('translate')
    )
);

$result = run($workflow,
    text: 'Hello world',
    lang: 'es',
    needsTranslation: true
);
```

---

Demo
----

[](#demo)

Run the included examples:

```
php demo/hello-world.php          # Basic workflow
php demo/chevere.php              # Chained jobs
php demo/closure.php              # Using closures
php demo/sync-vs-async.php        # Performance comparison
php demo/image-resize.php         # Parallel processing
php demo/run-if.php               # Conditional execution
```

See the [demo](demo) directory for all examples.

Documentation
-------------

[](#documentation)

Documentation is available at [chevere.org/packages/workflow](https://chevere.org/packages/workflow).

For a comprehensive introduction, read [Workflow for PHP](https://rodolfoberrios.com/2022/04/09/workflow-php/) on Rodolfo's blog.

License
-------

[](#license)

Copyright [Rodolfo Berrios A.](https://rodolfoberrios.com/)

This software is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

###  Health Score

55

—

FairBetter than 98% of packages

Maintenance87

Actively maintained with recent releases

Popularity34

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity70

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~34 days

Recently: every ~3 days

Total

45

Last Release

50d ago

Major Versions

0.9.0 → 1.0.02025-01-04

0.9.x-dev → 1.0.22025-01-11

1.0.x-dev → 2.0.02025-09-13

2.0.x-dev → 3.0.02026-02-21

### Community

Maintainers

![](https://www.gravatar.com/avatar/a87fbbcb1518fe64878bc53dedd9f61b81243cbb21b73c26f5e299d501375984?d=identicon)[rodolfo](/maintainers/rodolfo)

---

Top Contributors

[![rodber](https://avatars.githubusercontent.com/u/20590102?v=4)](https://github.com/rodber "rodber (419 commits)")

---

Tags

asynccheverephpworkflowworkflow-engine

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleECS

Type Coverage Yes

### Embed Badge

![Health badge](/badges/chevere-workflow/health.svg)

```
[![Health](https://phpackages.com/badges/chevere-workflow/health.svg)](https://phpackages.com/packages/chevere-workflow)
```

###  Alternatives

[amphp/process

A fiber-aware process manager based on Amp and Revolt.

25652.6M52](/packages/amphp-process)[amphp/parallel-functions

Parallel processing made simple.

27910.3M26](/packages/amphp-parallel-functions)[chevere/chevere

High quality library for building modern PHP

9718.9k4](/packages/chevere-chevere)[chevere/parameter

Dynamic parameter-argument validation for PHP with rich type constraints, attributes, and schema introspection.

1023.7k20](/packages/chevere-parameter)[amphp/react-adapter

Adapter to make any ReactPHP library compatible with Amp.

24180.0k4](/packages/amphp-react-adapter)[kelunik/retry

A tiny library for retrying failed operations.

1620.1k7](/packages/kelunik-retry)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
