{"id":27994498,"url":"https://github.com/chevere/workflow","last_synced_at":"2026-04-19T23:05:31.233Z","repository":{"id":54638537,"uuid":"455196315","full_name":"chevere/workflow","owner":"chevere","description":"Declarative workflow engine for PHP with automatic dependency resolution, sync/async job execution, and type-safe response chaining.","archived":false,"fork":false,"pushed_at":"2026-04-14T23:57:20.000Z","size":860,"stargazers_count":111,"open_issues_count":4,"forks_count":2,"subscribers_count":3,"default_branch":"3.0","last_synced_at":"2026-04-15T01:41:35.867Z","etag":null,"topics":["async","chevere","php","workflow","workflow-engine"],"latest_commit_sha":null,"homepage":"https://chevere.org/packages/workflow","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chevere.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-02-03T14:26:40.000Z","updated_at":"2026-04-14T23:57:24.000Z","dependencies_parsed_at":"2023-10-03T01:24:43.668Z","dependency_job_id":"c1b53837-fac5-4d04-bec1-42f821996c72","html_url":"https://github.com/chevere/workflow","commit_stats":{"total_commits":118,"total_committers":1,"mean_commits":118.0,"dds":0.0,"last_synced_commit":"25dc1ae9e60d725c9febd586ae76eb6b88c47efe"},"previous_names":[],"tags_count":34,"template":false,"template_full_name":"chevere/package-template","purl":"pkg:github/chevere/workflow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chevere%2Fworkflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chevere%2Fworkflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chevere%2Fworkflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chevere%2Fworkflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chevere","download_url":"https://codeload.github.com/chevere/workflow/tar.gz/refs/heads/3.0","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chevere%2Fworkflow/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32025786,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T20:23:30.271Z","status":"online","status_checked_at":"2026-04-19T02:00:07.110Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","chevere","php","workflow","workflow-engine"],"created_at":"2025-05-08T19:12:57.716Z","updated_at":"2026-04-19T23:05:31.225Z","avatar_url":"https://github.com/chevere.png","language":"PHP","readme":"# Workflow\n\n![Chevere](chevere.svg)\n\n[![Build](https://img.shields.io/github/actions/workflow/status/chevere/workflow/test.yml?branch=2.1\u0026style=flat-square)](https://github.com/chevere/workflow/actions)\n![Code size](https://img.shields.io/github/languages/code-size/chevere/workflow?style=flat-square)\n[![Apache-2.0](https://img.shields.io/github/license/chevere/workflow?style=flat-square)](LICENSE)\n[![PHPStan](https://img.shields.io/badge/PHPStan-level%209-blueviolet?style=flat-square)](https://phpstan.org/)\n[![Mutation testing badge](https://img.shields.io/endpoint?style=flat-square\u0026url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fchevere%2Fworkflow%2F2.1)](https://dashboard.stryker-mutator.io/reports/github.com/chevere/workflow/2.1)\n\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=chevere_workflow\u0026metric=alert_status)](https://sonarcloud.io/dashboard?id=chevere_workflow)\n[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=chevere_workflow\u0026metric=sqale_rating)](https://sonarcloud.io/dashboard?id=chevere_workflow)\n[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=chevere_workflow\u0026metric=reliability_rating)](https://sonarcloud.io/dashboard?id=chevere_workflow)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=chevere_workflow\u0026metric=security_rating)](https://sonarcloud.io/dashboard?id=chevere_workflow)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=chevere_workflow\u0026metric=coverage)](https://sonarcloud.io/dashboard?id=chevere_workflow)\n[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=chevere_workflow\u0026metric=sqale_index)](https://sonarcloud.io/dashboard?id=chevere_workflow)\n[![CodeFactor](https://www.codefactor.io/repository/github/chevere/workflow/badge)](https://www.codefactor.io/repository/github/chevere/workflow)\n\n## Summary\n\n**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.\n\n**Key features:**\n\n* **Declarative job definitions**: Define what to do, not how to orchestrate it\n* **Automatic dependency graph**: Jobs execute in optimal order based on their dependencies\n* **Sync and async execution**: Mix blocking and non-blocking jobs freely\n* **Type-safe responses**: Access job outputs with full type safety\n* **Conditional execution**: Run jobs based on variables or previous responses\n* **Built-in retry policies**: Handle transient failures automatically\n* **Testable**: Each job is independently testable and workflow graph can be verified\n\nYou define jobs and how they connect and depend on each other, **Chevere Workflow** figures out the execution order and runs them accordingly.\n\n## Integrations\n\n* **[VS Code Extension](https://marketplace.visualstudio.com/items?itemName=Chevere.vscode-workflow)**: Complete language server support plus graph visualization\n* **[Laravel Integration](https://chevere.org/packages/workflow-laravel)**: Package for integrating with Laravel applications\n\n## Installing\n\nWorkflow is available through [Packagist](https://packagist.org/packages/chevere/workflow) and the repository source is at [chevere/workflow](https://github.com/chevere/workflow).\n\n```sh\ncomposer require chevere/workflow\n```\n\n## Quick Start\n\nHere's a minimal example to get you started:\n\n```php\nuse function Chevere\\Workflow\\{workflow, sync, variable, run};\n\n// 1. Define a workflow with jobs\n$workflow = workflow(\n    greet: sync(\n        fn(string $name): string =\u003e \"Hello, {$name}!\",\n        name: variable('username')\n    )\n);\n\n// 2. Run with variables\n$result = run($workflow, username: 'World');\n\n// 3. Get typed responses\necho $result-\u003eresponse('greet')-\u003estring();\n// Output: Hello, World!\n```\n\n## Core Concepts\n\nWorkflow is built around four main concepts:\n\n| Concept      | Description                                         |\n| ------------ | --------------------------------------------------- |\n| **Job**      | A unit of work that produces a response             |\n| **Variable** | External input provided when running the workflow   |\n| **Response** | Reference to output from a previous job             |\n| **Graph**    | Automatic execution order based on job dependencies |\n\n### How It Works\n\n1. You define jobs using `sync()` or `async()` functions\n2. Jobs declare their inputs: literal values, `variable()` references, or `response()` from other jobs\n3. The engine builds a dependency graph automatically\n4. Jobs execute in optimal order (parallel when possible)\n5. Access typed responses after execution\n\n## Functions Reference\n\n| Function     | Purpose                                   |\n| ------------ | ----------------------------------------- |\n| `workflow()` | Create a workflow from named jobs         |\n| `sync()`     | Create a synchronous (blocking) job       |\n| `async()`    | Create an asynchronous (non-blocking) job |\n| `variable()` | Declare a runtime variable                |\n| `response()` | Reference another job's output            |\n| `run()`      | Execute a workflow with variables         |\n\n---\n\n## Jobs\n\nJobs 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.\n\n### Creating Jobs with Closures\n\nUse closures for simple, inline operations:\n\n```php\nuse function Chevere\\Workflow\\{workflow, sync, async, variable, response, run};\n\n$workflow = workflow(\n    // Simple calculation\n    add: sync(\n        fn(int $a, int $b): int =\u003e $a + $b,\n        a: 10,\n        b: variable('number')\n    ),\n    // Format the result\n    format: sync(\n        fn(int $sum): string =\u003e \"Sum: {$sum}\",\n        sum: response('add')\n    )\n);\n\n$result = run($workflow, number: 5);\necho $result-\u003eresponse('format')-\u003estring(); // Sum: 15\n```\n\n### Creating Jobs with Action Classes\n\nFor 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.\n\n```php\nuse Chevere\\Action\\Action;\n\nclass FetchUser extends Action\n{\n    public function __invoke(int $userId): array\n    {\n        // Fetch user from database\n        return ['id' =\u003e $userId, 'name' =\u003e 'John', 'email' =\u003e 'john@example.com'];\n    }\n}\n\nclass SendEmail extends Action\n{\n    public function __invoke(string $email, string $subject): bool\n    {\n        // Send email logic\n        return true;\n    }\n}\n```\n\n```php\n$workflow = workflow(\n    user: sync(\n        FetchUser::class,\n        userId: variable('id')\n    ),\n    notify: sync(\n        SendEmail::class,\n        email: response('user', 'email'),\n        subject: 'Welcome!'\n    )\n);\n\n$result = run($workflow, id: 123);\n```\n\n### Creating Jobs with Invocable Classes\n\nUse invocable classes (classes with `__invoke` method) for reusable logic without needing Action base class:\n\n```php\nclass CalculateTotal\n{\n    public function __invoke(array $items, float $taxRate): float\n    {\n        $subtotal = array_sum(array_column($items, 'price'));\n        return $subtotal * (1 + $taxRate);\n    }\n}\n\nclass FormatCurrency\n{\n    public function __invoke(float $amount, string $currency = 'USD'): string\n    {\n        return $currency . ' ' . number_format($amount, 2);\n    }\n}\n```\n\n```php\n$workflow = workflow(\n    total: sync(\n        CalculateTotal::class,\n        items: variable('items'),\n        taxRate: 0.08\n    ),\n    formatted: sync(\n        FormatCurrency::class,\n        amount: response('total'),\n        currency: 'EUR'\n    )\n);\n\n$result = run($workflow, items: [\n    ['name' =\u003e 'Item 1', 'price' =\u003e 10.00],\n    ['name' =\u003e 'Item 2', 'price' =\u003e 20.00]\n]);\necho $result-\u003eresponse('formatted')-\u003estring(); // EUR 32.40\n```\n\n### Creating Jobs with Callables\n\nUse any PHP callable including array callbacks, function names, or static methods:\n\n```php\nclass StringHelper\n{\n    public static function uppercase(string $text): string\n    {\n        return strtoupper($text);\n    }\n\n    public function reverse(string $text): string\n    {\n        return strrev($text);\n    }\n}\n```\n\n```php\n$helper = new StringHelper();\n\n$workflow = workflow(\n    // Using built-in PHP function\n    trim: sync(\n        'trim',\n        string: variable('input')\n    ),\n    // Using static method\n    upper: sync(\n        [StringHelper::class, 'uppercase'],\n        text: response('trim')\n    ),\n    // Using instance method\n    reversed: sync(\n        [$helper, 'reverse'],\n        text: response('upper')\n    )\n);\n\n$result = run($workflow, input: '  hello  ');\necho $result-\u003eresponse('reversed')-\u003estring(); // OLLEH\n```\n\n### Sync vs Async Jobs\n\n**Synchronous jobs** (`sync`) block execution until complete. Use for operations that must run in sequence:\n\n```php\nworkflow(\n    first: sync(ActionA::class),  // Runs first\n    second: sync(ActionB::class), // Waits for first\n    third: sync(ActionC::class),  // Waits for second\n);\n// Graph: first → second → third\n```\n\n**Asynchronous jobs** (`async`) run concurrently when they have no dependencies:\n\n```php\nworkflow(\n    resize1: async(ResizeImage::class, size: 'thumb'),\n    resize2: async(ResizeImage::class, size: 'medium'),\n    resize3: async(ResizeImage::class, size: 'large'),\n    store: sync(StoreFiles::class, files: response('resize1'), ...)\n);\n// Graph: [resize1, resize2, resize3] → store\n```\n\n### Job Arguments\n\nJobs accept three types of arguments:\n\n```php\nworkflow(\n    example: sync(\n        MyAction::class,\n        literal: 'fixed value',           // Literal value\n        dynamic: variable('userInput'),   // Runtime variable\n        chained: response('otherJob'),    // Previous job output\n    )\n);\n```\n\nJobs 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.\n\n### Integration with Chevere\n\nWorkflow 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.\n\nThese 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.\n\n---\n\n## Variables\n\nVariables are placeholders for values provided at runtime. Declare them with `variable()`:\n\n```php\n$workflow = workflow(\n    job1: sync(\n        SomeAction::class,\n        name: variable('userName'),\n        age: variable('userAge')\n    )\n);\n\n// Provide values when running\n$result = run($workflow, userName: 'Alice', userAge: 30);\n```\n\nAll declared variables must be provided when running the workflow.\n\n---\n\n## Responses\n\nUse `response()` to pass output from one job to another. This automatically establishes a dependency.\n\n```php\n$workflow = workflow(\n    fetch: sync(\n        FetchData::class,\n        url: variable('endpoint')\n    ),\n    process: sync(\n        ProcessData::class,\n        data: response('fetch')  // Gets entire response from 'fetch'\n    ),\n    extract: sync(\n        ExtractField::class,\n        value: response('fetch', 'items')  // Gets 'items' key from response\n    )\n);\n```\n\n### Accessing Nested Response Keys/Properties\n\nWhen a job returns `array` or `object`, access specific keys/properties directly in `response()`:\n\n```php\nresponse('user')           // job:user       Entire response object\nresponse('user', 'id')     // job:user-\u003eid   id property from object response\nresponse('post', 'id')     // job:post['id'] id key from array response\n```\n\n---\n\n## Execution Graph\n\nThe 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.\n\n```php\n$workflow = workflow(\n    // Independent async jobs run in parallel\n    thumb: async(ImageResize::class, size: 'thumb', file: variable('image')),\n    medium: async(ImageResize::class, size: 'medium', file: variable('image')),\n    large: async(ImageResize::class, size: 'large', file: variable('image')),\n    // Sync job waits for all above\n    store: sync(\n        StoreFiles::class,\n        thumb: response('thumb'),\n        medium: response('medium'),\n        large: response('large')\n    )\n);\n$graph = $workflow-\u003ejobs()-\u003egraph()-\u003etoArray();\n// [\n//     ['thumb', 'medium', 'large'],  // Level 0: parallel\n//     ['store']                      // Level 1: after dependencies\n// ]\n```\n\n```mermaid\ngraph TD\n    thumb --\u003e store\n    medium --\u003e store\n    large --\u003e store\n```\n\n---\n\n## Mermaid Graphs\n\nWorkflow'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.\n\nGenerate a Mermaid flowchart using `Mermaid::generate()`:\n\n```php\n$workflow = workflow(\n    ja: async(\n        fn (): int =\u003e 1\n    ),\n    jb: async(\n        fn (): int =\u003e 2\n    )\n        -\u003ewithRunIf(response('ja'))\n        -\u003ewithRunIfNot(variable('var')),\n    j1: async(\n        #[_return(new _arrayp(\n            id: new _int(),\n            name: new _string()\n        ))]\n        fn (): array =\u003e [\n            'id' =\u003e 123,\n            'name' =\u003e 'example',\n        ]\n    ),\n    j2: sync(\n        fn (int $n, string $m): int =\u003e $n + $m,\n        n: response('j1', 'id'),\n        m: response('j1', 'name')\n    ),\n    j3: sync(\n        fn (int $a): int =\u003e $a,\n        a: response('jb')\n    ),\n    j4: sync(\n        fn (int $i, int $j): int =\u003e $i * $j,\n        i: response('j2'),\n        j: response('j3')\n    ),\n);\n$mermaid = Mermaid::generate($workflow);\n```\n\n```mermaid\ngraph TB;\n    ja(\"`ja`\");\n    j1(\"`j1`\");\n    j2(\"`j2`\");\n    jb(\"`jb\n*if* res(ja)\n*ifNot* var(var)`\");\n    j3(\"`j3`\");\n    j4(\"`j4`\");\n\n    j1--\u003e|\"j1-\u003eid @ j2(n:)\nj1-\u003ename @ j2(m:)\"|j2;\n    ja--\u003ejb;\n    jb--\u003e|\"jb @ j3(a:)\"|j3;\n    j2--\u003e|\"j2 @ j4(i:)\"|j4;\n    j3--\u003e|\"j3 @ j4(j:)\"|j4;\n```\n\nWhere:\n\n* ***if* res(ja)**\n  Job `jb` runs only if job `ja` response is truthy\n* ***ifNot* var(var)**\n  Job `jb` runs only if `var` variable is not falsy\n* **j1-\u003eid @ j2(n:)**\n  Job `j1` response key/property `id` is used as argument `n` for job `j2`\n* **j1-\u003ename @ j2(m:)**\n  Job `j1` response key/property `name` is used as argument `m` for job `j2`\n* **jb @ j3(a:)**\n  Job `jb` response is used as argument `a` for job `j3`\n* **j2 @ j4(i:)**\n  Job `j2` response is used as argument `i` for job `j4`\n* **j3 @ j4(j:)**\n  Job `j3` response is used as argument `j` for job `j4`\n\n---\n\n## Running Workflows\n\nExecute a workflow with the `run()` function:\n\n```php\nuse function Chevere\\Workflow\\run;\n\n// Basic execution\n$result = run($workflow, var1: 'value1', var2: 'value2');\n\n// With dependency injection container\n$result = run($workflow, $container, var1: 'value1');\n```\n\n### Accessing Responses\n\nThe run result provides type-safe access to job responses:\n\n```php\n$result = run($workflow, ...);\n\n// Get typed responses\n$result-\u003eresponse('jobName')-\u003estring();     // string\n$result-\u003eresponse('jobName')-\u003eint();        // int\n$result-\u003eresponse('jobName')-\u003efloat();      // float\n$result-\u003eresponse('jobName')-\u003ebool();       // bool\n$result-\u003eresponse('jobName')-\u003earray();      // array\n\n// Access array keys directly\n$result-\u003eresponse('jobName', 'key')-\u003estring();\n$result-\u003eresponse('jobName', 'nested', 'key')-\u003eint();\n```\n\n### Check Skipped Jobs\n\nWhen using conditional execution, check which jobs were skipped:\n\n```php\nif ($result-\u003eskip()-\u003econtains('optionalJob')) {\n    // Job was skipped\n}\n```\n\n---\n\n## Workflow Provider Convention\n\nImplement `WorkflowProviderInterface` to expose a workflow definition from a class:\n\n```php\nuse Chevere\\Workflow\\Interfaces\\WorkflowProviderInterface;\nuse Chevere\\Workflow\\Interfaces\\WorkflowInterface;\n\nclass MyProvider implements WorkflowProviderInterface\n{\n    public static function workflow(): WorkflowInterface\n    {\n        return workflow(/* ... */);\n    }\n}\n```\n\nThis is the recommended pattern for packages and applications. It separates workflow configuration from execution logic and enables discovery by tooling such as the **Chevere Workflow VSCode extension**.\n\n## Workflow Discovery\n\n`WorkflowDiscovery` provides a list for classes implementing `WorkflowProviderInterface` under `providers()`, and a list of all class-string dependencies (jobs) under `dependencies()`.\n\n### Creating WorkflowDiscovery\n\nCreate a discovery instance by providing the path to the directory containing your workflow providers:\n\n```php\nuse Chevere\\Workflow\\WorkflowDiscovery;\n\n$discovery = WorkflowDiscovery::fromDirectory('/path/to/src');\n\n// `workflow()` providers e.g. [OrderWorkflow::class, UserWorkflow::class, ...]\n$workflowProviders = $discovery-\u003eproviders();\n\n// Job action classes e.g. [UserCreate::class, OrderCreate::class, ...]\n$workflowDependencies = $discovery-\u003edependencies();\n```\n\n### Build WorkflowDiscovery\n\nCall `build()` to persist the discovery results as PHP return files, which you can commit to your repository or load at runtime for faster access without needing to scan directories:\n\n```php\n$discovery-\u003ebuild('/path/to/build');\n```\n\nTwo files are written:\n\n| File                        | Contents                                                   |\n| --------------------------- | ---------------------------------------------------------- |\n| `workflow-providers.php`    | `array\u003cclass-string\u003cWorkflowProviderInterface\u003e\u003e` providers |\n| `workflow-dependencies.php` | `array\u003cclass-string\u003e` required by discovered job actions   |\n\nCall `fromBuild()` to load the discovery results from the cache files:\n\n```php\n$discovery = WorkflowDiscovery::fromBuild('/path/to/build');\n```\n\n### Validating Dependencies\n\nTo validate that your container can satisfy all discovered dependencies before running any workflow, call `assert()` on `Dependencies` with your container instance. It will throw if any required class is missing:\n\n```php\n$dependencies = new Dependencies(\n    ...$discovery-\u003edependencies(),\n    ...$discovery-\u003eproviders()\n);\n$dependencies-\u003eassert($container);\n```\n\nYou can also manually check the list of dependencies against your PSR-11 container:\n\n```php\nforeach ($discovery-\u003edependencies() as $class) {\n    if (! $container-\u003ehas($class)) {\n        throw new RuntimeException(\"Missing dependency: {$class}\");\n    }\n}\n```\n\n---\n\n## Dependency Injection\n\nWorkflow 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.\n\n### Passing a Container\n\nPass a `ContainerInterface` instance as the second argument to `run()`:\n\n```php\nuse Chevere\\Container\\Container; // or any PSR-11 container\nuse function Chevere\\Workflow\\run;\n\n// Create container with dependencies\n$container = new Container(\n    logger: new Logger(),\n    database: new Database()\n);\n\n// Run workflow with container\n// When using chevere/container it will auto-inject and assert\n$result = run($workflow, $container, ...$vars);\n```\n\nWhen a job references a class-string (Action class, invokable class, or any other class), Workflow uses the container to:\n\n1. **Inject dependencies** - Automatically resolve constructor parameters from the container\n2. **Validate availability** - Ensure all required dependencies are present before execution\n3. **Support nested dependencies** - Recursively resolve dependencies of dependencies\n\n**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.\n\n### Example with Action Dependencies\n\n```php\nuse Chevere\\Action\\Action;\n\nclass SendNotification extends Action\n{\n    // Dependencies injected automatically\n    public function __construct(\n        private LoggerInterface $logger,\n        private MailerInterface $mailer\n    ) {}\n\n    public function __invoke(string $email, string $message): bool\n    {\n        $this-\u003elogger-\u003einfo(\"Sending email to {$email}\");\n        return $this-\u003emailer-\u003esend($email, $message);\n    }\n}\n\n// Provide dependencies in container\n$container = new Container(\n    logger: new ConsoleLogger(),\n    mailer: new SmtpMailer()\n);\n\n$workflow = workflow(\n    notify: sync(\n        SendNotification::class,  // Dependencies auto-injected\n        email: variable('userEmail'),\n        message: 'Welcome!'\n    )\n);\n\n$result = run($workflow, $container, userEmail: 'user@example.com');\n```\n\n### Example with Invokable Class Dependencies\n\nDependency injection also works with invokable classes and any other class:\n\n```php\nclass ProcessOrder\n{\n    // Dependencies injected automatically\n    public function __construct(\n        private DatabaseInterface $database,\n        private PaymentGateway $payment\n    ) {}\n\n    public function __invoke(int $orderId, float $amount): array\n    {\n        $order = $this-\u003edatabase-\u003egetOrder($orderId);\n        $result = $this-\u003epayment-\u003echarge($amount);\n        return ['order' =\u003e $order, 'payment' =\u003e $result];\n    }\n}\n\n// Provide dependencies in container\n$container = new Container(\n    database: new MySQLDatabase(),\n    payment: new StripeGateway()\n);\n\n$workflow = workflow(\n    process: sync(\n        ProcessOrder::class,  // Dependencies auto-injected\n        orderId: variable('orderId'),\n        amount: variable('amount')\n    )\n);\n\n$result = run($workflow, $container, orderId: 123, amount: 99.99);\n```\n\n---\n\n## Conditional Execution\n\nControl 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.\n\n### Accepted condition types\n\n* `boolean|int|float|string` literal scalar value\n* `variable('name')` runtime argument coerced to truly/falsy\n* `response('job')` or `response('job', 'key')` uses another job's output\n* `callable` invokes a callable passing the current `RunInterface` context argument\n\n**Note:** Empty string is considered falsy. To learn more check [PHP type comparison tables](https://www.php.net/manual/en/types.comparisons.php).\n\n---\n\n```php\nuse function Chevere\\Workflow\\{workflow, sync, variable, run, response};\n\n$workflow = workflow(\n    isTooBig: sync(\n        fn(string $path, int $maxBytes): bool =\u003e filesize($path) \u003e $maxBytes,\n        path: variable('file'),\n        maxBytes: variable('maxBytes')\n    ),\n    compress: sync(\n        CompressImage::class,\n        file: variable('file')\n    )-\u003ewithRunIf(\n        true,                       // literal\n        variable('shouldCompress'), // workflow variable\n        response('isTooBig'),       // job response value\n        fn(RunInterface $run) =\u003e $run-\u003evariable('shouldCompress')-\u003ebool(), // closure condition variable\n        fn(RunInterface $run) =\u003e $run-\u003eresponse('isTooBig')-\u003ebool(), // closure condition using response\n    )\n);\n$result = run($workflow,\n    file: '/path/to/image.jpg',\n    shouldCompress: true,\n    maxBytes: 1_000_000\n);\n```\n\n---\n\n## Explicit Dependencies\n\nWhile `response()` creates implicit dependencies, use `withDepends()` for explicit control:\n\n```php\n$workflow = workflow(\n    exists: sync(ExistsAction::class),\n    update: sync(UpdateAction::class)\n        -\u003ewithRunIf(response('exists')),\n    cleanup: sync(CleanupAction::class)\n        -\u003ewithDepends('update')\n);\n```\n\nFor the code above, `cleanup` happens only if `update` runs and completes successfully. If `update` is skipped (because `exists` is false), then `cleanup` is also skipped since it depends on `update`.\n\n---\n\n## Run After Job\n\nUse `withAfter()` to enforce ordering between jobs without creating a dependency. It guarantees that the target job is scheduled only after the specified job node has resolved.\n\n```php\n$workflow = workflow(\n    exists: sync(ExistsAction::class),\n    update: sync(UpdateAction::class)\n        -\u003ewithRunIf(response('exists')),\n    cleanup: sync(CleanupAction::class)\n        -\u003ewithAfter('update')\n);\n```\n\nFor the code above, `cleanup` happens after node `update` regardless of whether `update` actually ran or was skipped.\n\nWithout `withAfter('update')`, `cleanup` and `update` are independent and may run in any order (for example, `cleanup` could run before `update`). Adding `withAfter('update')` ensures `cleanup` is scheduled only after the `update` node resolves.\n\n---\n\n## Retry Policy\n\nConfigure automatic retries for jobs that may fail transiently:\n\n```php\n$workflow = workflow(\n    fetch: sync(\n        FetchFromApi::class,\n        url: variable('endpoint')\n    )-\u003ewithRetry(\n        timeout: 300,     // Max 300 seconds total\n        maxAttempts: 5,   // Try up to 5 times\n        delay: 10         // Wait 10 seconds between attempts\n    )\n);\n```\n\n| Parameter     | Type          | Default | Description                                   |\n| ------------- | ------------- | ------- | --------------------------------------------- |\n| `timeout`     | `int\u003c0, max\u003e` | `0`     | Max execution time in seconds (0 = unlimited) |\n| `maxAttempts` | `int\u003c1, max\u003e` | `1`     | Total attempts including initial              |\n| `delay`       | `int\u003c0, max\u003e` | `0`     | Seconds between retries (0 = immediate)       |\n\nRetry delays use non-blocking sleep, making them safe for async runtimes.\n\n---\n\n## Exception Handling\n\nWhen a job fails, a `WorkflowException` wraps the original exception:\n\n```php\nuse Chevere\\Workflow\\Exceptions\\WorkflowException;\n\ntry {\n    $result = run($workflow, ...);\n} catch (WorkflowException $e) {\n    echo $e-\u003ename;        // Name of the failed job\n    echo $e-\u003ejob;         // Job instance\n    echo $e-\u003ethrowable;   // Original exception\n}\n```\n\n---\n\n## Return Early\n\nThrow `EarlyReturnException` inside a job to stop workflow execution immediately without treating it as an error. Catch it at the call site to handle the early exit gracefully:\n\n```php\nuse Chevere\\Workflow\\Exceptions\\WorkflowException;\nuse Chevere\\Workflow\\Exceptions\\EarlyReturnException;\n\ntry {\n    $result = run($workflow, ...);\n} catch (WorkflowException $e) {\n    if($e-\u003ethrowable instanceof EarlyReturnException) {\n        // Handle early return (e.g., return a default response)\n        return;\n    }\n}\n```\n\n---\n\n## Using WorkflowTrait\n\nFor class-based workflow management, use `WorkflowTrait`:\n\n```php\nuse Chevere\\Workflow\\Traits\\WorkflowTrait;\nuse function Chevere\\Workflow\\{workflow, sync, variable};\n\nclass OrderProcessor\n{\n    use WorkflowTrait;\n\n    public function process(int $orderId): void\n    {\n        $workflow = workflow(\n            validate: sync(ValidateOrder::class, id: variable('orderId')),\n            charge: sync(ChargePayment::class, order: response('validate')),\n            fulfill: sync(FulfillOrder::class, order: response('charge'))\n        );\n\n        $this-\u003eexecute($workflow, orderId: $orderId);\n    }\n\n    public function getResult(): string\n    {\n        return $this-\u003erun()-\u003eresponse('fulfill')-\u003estring();\n    }\n}\n```\n\n---\n\n## Lint Mode\n\nSet the `CHEVERE_WORKFLOW_LINT_ENABLE=1` environment variable to enable lint mode. In this mode both `Workflow` and `Job` collect parameter violations instead of throwing on errors, and generate a Mermaid graph on construction.\n\n```sh\nCHEVERE_WORKFLOW_LINT_ENABLE=1 php my-workflow.php\n```\n\nCall `$workflow-\u003elint()` to get a JSON report with violations and the Mermaid diagram:\n\n```php\n$workflow = workflow(\n    step: sync(MyAction::class, value: variable('input'))\n);\n\n$report = $workflow-\u003elint();\n// {\n//   \"violations\": [...],\n//   \"mermaid\": \"graph TB;\\n    ...\"\n// }\n```\n\nLint mode is intended for development and CI pipelines to inspect workflow definitions without halting on the first error.\n\nThe output conforms to the `schema/workflow-lint.schema.json`, which you can use to validate lint reports or integrate with tooling.\n\n---\n\n## Testing\n\n### Testing Actions\n\nTest your Action classes independently:\n\n```php\nuse PHPUnit\\Framework\\TestCase;\n\nclass FetchUserTest extends TestCase\n{\n    public function testFetchUser(): void\n    {\n        $action = new FetchUser();\n        $result = $action(userId: 123);\n\n        $this-\u003eassertSame(123, $result['id']);\n        $this-\u003eassertArrayHasKey('name', $result);\n    }\n}\n```\n\n### Testing Workflow Graph\n\nVerify execution order:\n\n```php\npublic function testWorkflowGraph(): void\n{\n    $workflow = workflow(\n        a: async(ActionA::class),\n        b: async(ActionB::class),\n        c: sync(ActionC::class, x: response('a'), y: response('b'))\n    );\n    $graph = $workflow-\u003ejobs()-\u003egraph()-\u003etoArray();\n\n    $this-\u003eassertSame([['a', 'b'], ['c']], $graph);\n}\n```\n\n### Testing Workflow Providers with PHPUnit\n\nUse `Chevere\\Workflow\\Traits\\WorkflowProviderTestTrait` in PHPUnit test cases to assert provider correctness:\n\n| Method                                      | Description                                                    |\n| ------------------------------------------- | -------------------------------------------------------------- |\n| `assertWorkflowProvider($provider)`         | Asserts the class implements `WorkflowProviderInterface`       |\n| `assertWorkflowGraph($expected, $workflow)` | Asserts the workflow jobs dependency graph matches `$expected` |\n\nWhen passing a class string to `assertWorkflowGraph`, it also calls `assertWorkflowProvider` internally.\n\n```php\nuse Chevere\\Workflow\\Traits\\WorkflowProviderTestTrait;\n\nclass MyWorkflowProviderTest extends PHPUnit\\Framework\\TestCase\n{\n    use WorkflowProviderTestTrait;\n\n    public function testProviderGraph(): void\n    {\n        $this-\u003eassertWorkflowGraph(\n            [['a', 'b'], ['c']],\n            MyProvider::class\n        );\n    }\n}\n```\n\n### Testing Responses\n\nTest complete workflow execution:\n\n```php\npublic function testWorkflowResponses(): void\n{\n    $result = run($workflow, input: 'test');\n\n    $this-\u003eassertSame('expected', $result-\u003eresponse('job1')-\u003estring());\n    $this-\u003eassertSame(42, $result-\u003eresponse('job2', 'count')-\u003eint());\n}\n```\n\n### Testing Exceptions\n\nUse `ExpectWorkflowExceptionTrait` for error scenarios:\n\n```php\nuse Chevere\\Workflow\\Traits\\ExpectWorkflowExceptionTrait;\n\nclass WorkflowExceptionTest extends TestCase\n{\n    use ExpectWorkflowExceptionTrait;\n\n    public function testJobFailure(): void\n    {\n        $this-\u003eexpectWorkflowException(\n            closure: fn() =\u003e run($workflow, input: 'invalid'),\n            exception: InvalidArgumentException::class,\n            job: 'validate',\n            message: 'Invalid input provided'\n        );\n    }\n}\n```\n\n---\n\n## Real-World Examples\n\n### Image Processing Pipeline\n\n```php\n$workflow = workflow(\n    // Parallel image resizing\n    thumb: async(\n        ImageResize::class,\n        file: variable('image'),\n        width: 150,\n        height: 150\n    ),\n    medium: async(\n        ImageResize::class,\n        file: variable('image'),\n        width: 800\n    ),\n    // Store after all resizing completes\n    store: sync(\n        StoreFiles::class,\n        thumb: response('thumb'),\n        medium: response('medium'),\n        directory: variable('outputDir')\n    )\n);\n\n$result = run($workflow,\n    image: '/uploads/photo.jpg',\n    outputDir: '/processed/'\n);\n```\n\n### User Registration Flow\n\n```php\n$workflow = workflow(\n    validate: sync(\n        ValidateRegistration::class,\n        email: variable('email'),\n        password: variable('password')\n    ),\n    createUser: sync(\n        CreateUser::class,\n        data: response('validate')\n    ),\n    sendWelcome: async(\n        SendWelcomeEmail::class,\n        user: response('createUser')\n    ),\n    logEvent: async(\n        LogRegistration::class,\n        userId: response('createUser', 'id')\n    )\n);\n```\n\n### Conditional Processing\n\n```php\n$workflow = workflow(\n    analyze: sync(\n        AnalyzeContent::class,\n        content: variable('text')\n    ),\n    translate: sync(\n        TranslateContent::class,\n        text: variable('text'),\n        targetLang: variable('lang')\n    )-\u003ewithRunIf(\n        variable('needsTranslation')\n    ),\n    publish: sync(\n        PublishContent::class,\n        content: response('analyze'),\n        translated: response('translate')\n    )\n);\n\n$result = run($workflow,\n    text: 'Hello world',\n    lang: 'es',\n    needsTranslation: true\n);\n```\n\n---\n\n## Demo\n\nRun the included examples:\n\n```sh\nphp demo/hello-world.php          # Basic workflow\nphp demo/chevere.php              # Chained jobs\nphp demo/closure.php              # Using closures\nphp demo/sync-vs-async.php        # Performance comparison\nphp demo/image-resize.php         # Parallel processing\nphp demo/run-if.php               # Conditional execution\n```\n\nSee the [demo](demo) directory for all examples.\n\n## Documentation\n\nDocumentation is available at [chevere.org/packages/workflow](https://chevere.org/packages/workflow).\n\nFor a comprehensive introduction, read [Workflow for PHP](https://rodolfoberrios.com/2022/04/09/workflow-php/) on Rodolfo's blog.\n\n## License\n\nCopyright [Rodolfo Berrios A.](https://rodolfoberrios.com/)\n\nThis software is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text.\n\nUnless 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.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchevere%2Fworkflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchevere%2Fworkflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchevere%2Fworkflow/lists"}