Skip to main content
This page showcases real-world examples from the Chevere Workflow repository, demonstrating common patterns and best practices.

Hello world

The simplest workflow example using an Action class:
demo/hello-world.php
use Chevere\Demo\Actions\Greet;
use function Chevere\Workflow\{run, sync, variable, workflow};

$workflow = workflow(
    greet: sync(
        new Greet(),
        username: variable('username'),
    ),
);

$run = run(
    $workflow,
    username: $argv[1] ?? 'World'
);

echo $run->response('greet')->string();
// Output: Hello, World!
The Greet action:
demo/Actions/Greet.php
use Chevere\Action\Action;
use Chevere\Parameter\Interfaces\StringParameterInterface;
use function Chevere\Parameter\string;

class Greet extends Action
{
    public function __invoke(string $username): string
    {
        return "Hello, {$username}!";
    }

    public static function acceptReturn(): StringParameterInterface
    {
        return string('/^Hello, /');
    }
}
The acceptReturn() method defines validation rules for the return value using chevere/parameter. The response must be a string starting with “Hello, ”.

Chained jobs

Passing data between jobs using response():
demo/chevere.php
use Chevere\Demo\Actions\MyAction;
use function Chevere\Workflow\{response, run, sync, variable, workflow};

$workflow = workflow(
    greet: sync(
        MyAction::class,
        foo: variable('super'),
    ),
    capo: sync(
        MyAction::class,
        foo: response('greet'),
    ),
    wea: sync(
        function (string $foo) {
            return "Wea, {$foo}";
        },
        foo: response('greet'),
    ),
);

$hello = run(
    $workflow,
    super: 'Chevere',
);

echo $hello->response('greet')->string() . PHP_EOL;
// Hello, Chevere

echo $hello->response('capo')->string() . PHP_EOL;
// Hello, Hello, Chevere

echo $hello->response('wea')->string() . PHP_EOL;
// Wea, Hello, Chevere
use Chevere\Action\Action;

class MyAction extends Action
{
    public function __invoke(string $foo): string
    {
        return "Hello, {$foo}";
    }
}

Using closures

Simple calculations using inline closures:
demo/closure.php
use function Chevere\Workflow\{response, run, sync, variable, workflow};

$workflow = workflow(
    calculate: sync(
        function (int $a, int $b): int {
            return $a + $b;
        },
        a: 10,
        b: variable('value')
    ),
    format: sync(
        fn (int $result): string => "Result: {$result}",
        result: response('calculate')
    )
);

$run = run($workflow, value: 5);
echo $run->response('format')->string() . PHP_EOL;
// Result: 15
Closures are perfect for simple, one-off operations. For complex or reusable logic, use Action classes instead.

Parallel image processing

Resize images in parallel, then store the results:
demo/image-resize.php
use Chevere\Demo\Actions\{ImageResize, StoreFile};
use function Chevere\Workflow\{async, response, run, variable, workflow};

$workflow = workflow(
    thumb: async(
        new ImageResize(),
        file: variable('image'),
        fit: 'thumbnail',
    ),
    poster: async(
        new ImageResize(),
        file: variable('image'),
        fit: 'poster',
    ),
    storeThumb: async(
        new StoreFile(),
        file: response('thumb'),
        dir: variable('saveDir'),
    ),
    storePoster: async(
        new StoreFile(),
        file: response('poster'),
        dir: variable('saveDir'),
    )
);

$run = run(
    $workflow,
    image: __DIR__ . '/src/php.jpeg',
    saveDir: __DIR__ . '/src/output/',
);

// View execution graph
$graph = $run->workflow()->jobs()->graph()->toArray();
echo "Workflow graph:\n";
foreach ($graph as $level => $jobs) {
    echo " {$level}: " . implode('|', $jobs) . "\n";
}

// Output:
// Workflow graph:
//  0: thumb|poster
//  1: storeThumb|storePoster

ImageResize action

demo/Actions/ImageResize.php
use Chevere\Action\Action;
use Chevere\Parameter\Attributes\_string;
use RuntimeException;

class ImageResize extends Action
{
    public const FIT_WIDTH = [
        'thumbnail' => 50,
        'poster' => 150,
    ];

    public function __invoke(
        #[_string('/\.jpe?g$/')]
        string $file,
        string $fit
    ): string {
        [$width, $height] = getimagesize($file);
        $targetWidth = self::FIT_WIDTH[$fit];
        $targetHeight = intval($height / $width * $targetWidth);
        
        $image = imagecreatetruecolor($targetWidth, $targetHeight);
        $source = imagecreatefromjpeg($file);
        imagecopyresampled(
            $image, $source, 0, 0, 0, 0,
            $targetWidth, $targetHeight, $width, $height
        );
        
        $pos = strrpos($file, '.');
        $target = substr($file, 0, $pos)
            . ".{$fit}"
            . substr($file, $pos);

        return imagejpeg($image, $target)
            ? $target
            : throw new RuntimeException('Unable to save image');
    }
}
The #[_string('/\.jpe?g$/')] attribute validates that the file parameter ends with .jpg or .jpeg.

Conditional execution

Run jobs based on runtime conditions:
demo/run-if.php
use Chevere\Demo\Actions\Greet;
use function Chevere\Workflow\{run, sync, variable, workflow};

$workflow = workflow(
    greet: sync(
        new Greet(),
        username: variable('username'),
    )->withRunIf(
        variable('sayHello')
    ),
);

$name = $argv[1] ?? '';
$run = run(
    $workflow,
    username: $name,
    sayHello: $name !== ''
);

if ($run->skip()->contains('greet')) {
    exit;
}

echo $run->response('greet')->string();
Usage:
php demo/run-if.php Rodolfo
# Output: Hello, Rodolfo!

php demo/run-if.php
# No output (job skipped)

Sync vs async performance

Compare synchronous and asynchronous execution:
demo/sync-vs-async.php
use Chevere\Demo\Actions\FetchUrl;
use function Chevere\Workflow\{async, run, sync, variable, workflow};

$sync = workflow(
    job1: sync(
        new FetchUrl(),
        url: variable('php'),
    ),
    job2: sync(
        new FetchUrl(),
        url: variable('github'),
    ),
    job3: sync(
        new FetchUrl(),
        url: variable('chevere'),
    ),
);

$async = workflow(
    job1: async(
        new FetchUrl(),
        url: variable('php'),
    ),
    job2: async(
        new FetchUrl(),
        url: variable('github'),
    ),
    job3: async(
        new FetchUrl(),
        url: variable('chevere'),
    ),
);

$variables = [
    'php' => 'https://www.php.net',
    'github' => 'https://github.com/chevere/workflow',
    'chevere' => 'https://chevere.org',
];

// Measure sync execution
$time = microtime(true);
$run = run($sync, ...$variables);
$time = round((microtime(true) - $time) * 1000);
echo "Time (ms)  sync: {$time}\n";

// Measure async execution
$time = microtime(true);
$run = run($async, ...$variables);
$time = round((microtime(true) - $time) * 1000);
echo "Time (ms) async: {$time}\n";
Typical output:
Time (ms)  sync: 842
Time (ms) async: 312
Async jobs execute in parallel when they have no dependencies, significantly reducing total execution time for I/O-bound operations.

Complex dependency graph

Building a multi-level workflow with mixed dependencies:
use function Chevere\Workflow\{workflow, async, sync, response, variable};

$workflow = workflow(
    // Level 0: Independent jobs run in parallel
    fetchUser: async(
        FetchUser::class,
        userId: variable('userId')
    ),
    fetchSettings: async(
        FetchSettings::class,
        userId: variable('userId')
    ),
    fetchPosts: async(
        FetchPosts::class,
        userId: variable('userId')
    ),
    
    // Level 1: Jobs that depend on level 0
    enrichUser: async(
        EnrichUserData::class,
        user: response('fetchUser'),
        settings: response('fetchSettings')
    ),
    
    // Level 2: Sequential job waits for enrichUser
    processUser: sync(
        ProcessUserData::class,
        user: response('enrichUser')
    ),
    
    // Level 3: Final jobs run in parallel after processUser
    sendEmail: async(
        SendWelcomeEmail::class,
        email: response('processUser', 'email')
    ),
    updateCache: async(
        UpdateUserCache::class,
        userId: variable('userId'),
        data: response('processUser')
    )
);

$graph = $workflow->jobs()->graph()->toArray();
print_r($graph);
// [
//     ['fetchUser', 'fetchSettings', 'fetchPosts'],  // Level 0
//     ['enrichUser'],                                 // Level 1
//     ['processUser'],                                // Level 2
//     ['sendEmail', 'updateCache']                    // Level 3
// ]

User registration flow

A complete user registration workflow:
use function Chevere\Workflow\{workflow, sync, async, variable, response, run};

$workflow = workflow(
    validate: sync(
        ValidateRegistration::class,
        email: variable('email'),
        password: variable('password')
    ),
    createUser: sync(
        CreateUser::class,
        email: variable('email'),
        passwordHash: response('validate', 'hash')
    ),
    sendWelcome: async(
        SendWelcomeEmail::class,
        userId: response('createUser', 'id'),
        email: variable('email')
    ),
    logEvent: async(
        LogRegistration::class,
        userId: response('createUser', 'id'),
        timestamp: response('createUser', 'createdAt')
    ),
    createProfile: async(
        CreateUserProfile::class,
        userId: response('createUser', 'id')
    )
);

$result = run(
    $workflow,
    email: '[email protected]',
    password: 'secure_password_123'
);

return [
    'userId' => $result->response('createUser', 'id')->int(),
    'email' => $result->response('createUser', 'email')->string(),
    'profileCreated' => !$result->skip()->contains('createProfile'),
    'emailSent' => !$result->skip()->contains('sendWelcome')
];

Content processing pipeline

Conditional processing with translation:
use function Chevere\Workflow\{workflow, sync, async, variable, response, run};

$workflow = workflow(
    analyze: sync(
        AnalyzeContent::class,
        content: variable('text')
    ),
    moderate: sync(
        ModerateContent::class,
        analysis: response('analyze')
    ),
    translate: async(
        TranslateContent::class,
        text: variable('text'),
        targetLang: variable('lang')
    )->withRunIf(
        variable('needsTranslation')
    ),
    generateSummary: async(
        GenerateSummary::class,
        content: variable('text')
    ),
    publish: sync(
        PublishContent::class,
        original: variable('text'),
        translated: response('translate'),
        summary: response('generateSummary'),
        moderation: response('moderate')
    )
);

$result = run(
    $workflow,
    text: 'Hello world, this is a test article...',
    lang: 'es',
    needsTranslation: true
);

if ($result->skip()->contains('translate')) {
    echo "Translation was skipped\n";
} else {
    echo "Translated to: " . $result->response('translate')->string() . "\n";
}

Testing workflow graphs

From the test suite - verifying execution order:
tests/GraphTest.php
use PHPUnit\Framework\TestCase;
use Chevere\Workflow\Graph;
use function Chevere\Workflow\async;

class GraphTest extends TestCase
{
    public function testMixedParallelAndSequential(): void
    {
        $graph = new Graph();
        $graph = $graph->withPut(
            'jn',
            $this->getJob()->withDepends('j0', 'j1')
        );
        $graph = $graph->withPut(
            'jx',
            $this->getJob()->withDepends('j0', 'j1')
        );
        $graph = $graph->withPut('j0', $this->getJob()->withIsSync(true));
        $graph = $graph->withPut('j1', $this->getJob()->withIsSync(true));

        $this->assertSame(
            [
                ['j0'],           // Level 0: First sync job
                ['j1'],           // Level 1: Second sync job  
                ['jn', 'jx'],     // Level 2: Parallel async jobs
            ],
            $graph->toArray()
        );
    }
}
Use graph()->toArray() to inspect and verify the execution order in your tests.

Best practices from examples

1
Use async for I/O operations
2
Network requests, file operations, and database queries benefit from async execution:
3
workflow(
    fetchApi1: async(FetchApi::class, url: 'https://api1.com'),
    fetchApi2: async(FetchApi::class, url: 'https://api2.com'),
    combine: sync(
        CombineResults::class,
        a: response('fetchApi1'),
        b: response('fetchApi2')
    )
)
4
Use sync for sequential operations
5
When order matters or resources conflict, use sync:
6
workflow(
    lock: sync(AcquireLock::class, resource: variable('id')),
    update: sync(UpdateResource::class, id: variable('id')),
    unlock: sync(ReleaseLock::class, resource: variable('id'))
)
7
Keep actions focused
8
Each action should have a single responsibility:
9
// Good: Focused actions
workflow(
    validate: sync(ValidateEmail::class, email: variable('email')),
    send: sync(SendEmail::class, to: variable('email'))
)

// Avoid: Do-everything actions
workflow(
    process: sync(ValidateAndSendEmail::class, email: variable('email'))
)
10
Use conditional execution wisely
11
Skip expensive operations when not needed:
12
workflow(
    checkCache: sync(CheckCache::class, key: variable('key')),
    fetchData: async(
        FetchFromDatabase::class,
        key: variable('key')
    )->withRunIfNot(
        response('checkCache', 'found')
    )
)

Build docs developers (and LLMs) love