Skip to main content
Chevere Workflow allows you to conditionally execute jobs based on variables, response values, or custom logic. This is useful for creating dynamic workflows that adapt to different scenarios.

Basic conditional execution

Use withRunIf() to execute a job only when a condition is true:
use function Chevere\Workflow\run;
use function Chevere\Workflow\sync;
use function Chevere\Workflow\variable;
use function Chevere\Workflow\workflow;

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

$run = run(
    $workflow,
    username: 'Rodolfo',
    sayHello: true
);

// Job executed
$greet = $run->response('greet')->string();

Conditional types

Jobs support four types of conditions:

Boolean values

The simplest condition is a direct boolean:
$job = async(new MyAction())
    ->withRunIf(true);  // Always runs

$job = async(new MyAction())
    ->withRunIf(false); // Never runs

Variables

Check workflow variables at runtime:
demo/run-if.php
use Chevere\Demo\Actions\Greet;
use function Chevere\Workflow\run;
use function Chevere\Workflow\sync;
use function Chevere\Workflow\variable;
use function Chevere\Workflow\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')) {
    echo "Greeting skipped\n";
    exit;
}

$greet = $run->response('greet')->string();
echo "{$greet}\n";

Response references

Condition on values from previous jobs:
$workflow = workflow(
    job1: async(new CheckPermissions()),
    job2: async(new ProcessData())
        ->withRunIf(
            response('job1', 'hasPermission')
        ),
);
You can reference the entire response (must return bool):
$job1 = sync(function (): bool {
    return true;
});

$job2 = async(new MyAction())
    ->withRunIf(response('job1'));

Callables

Use closures for complex conditional logic:
$workflow = workflow(
    job1: async(new GetUserData()),
    job2: async(new SendEmail())
        ->withRunIf(
            fn(RunInterface $run) => 
                $run->response('job1', 'age')->int() >= 18
        ),
);

Negative conditions

Use withRunIfNot() to execute a job when a condition is false:
$workflow = workflow(
    check: async(new ValidateInput()),
    error: async(new HandleError())
        ->withRunIfNot(
            response('check', 'isValid')
        ),
);

Multiple conditions

You can specify multiple conditions. ALL conditions must be true for the job to execute:
$workflow = workflow(
    job1: async(new CheckAuth()),
    job2: async(new CheckQuota()),
    process: async(new ProcessRequest())
        ->withRunIf(
            response('job1', 'authenticated'),
            response('job2', 'hasQuota'),
            variable('enabled')
        ),
);
When using multiple conditions with withRunIf(), the job runs only if ALL conditions are true. With withRunIfNot(), the job runs only if ALL conditions are false.

Combining conditions

You can use both withRunIf() and withRunIfNot() on the same job:
$job = async(new MyAction())
    ->withRunIf(variable('featureEnabled'))
    ->withRunIfNot(variable('maintenanceMode'));

Checking skipped jobs

The run instance tracks which jobs were skipped:
$run = run($workflow, enabled: false);

if ($run->skip()->contains('job1')) {
    echo "Job was skipped\n";
}

// Get all skipped jobs
$skipped = $run->skip()->toArray();
print_r($skipped);

Accessing skipped job responses

Attempting to access a skipped job’s response throws an exception:
use OutOfBoundsException;

$run = run($workflow, enabled: false);

try {
    $response = $run->response('skippedJob');
} catch (OutOfBoundsException $e) {
    // Job was skipped, no response available
}

Dependent jobs

If a job depends on a skipped job, it will also be skipped:
src/Runner.php
foreach ($job->dependencies() as $dependency) {
    try {
        $new->run()->response($dependency);
    } catch (OutOfBoundsException) {
        $new->addJobSkip($name);
        return $new;
    }
}
Example:
$workflow = workflow(
    job1: async(new MyAction())
        ->withRunIf(variable('enabled')),
    job2: async(new OtherAction())
        ->withDepends('job1'),
);

$run = run($workflow, enabled: false);

// Both jobs are skipped
assert($run->skip()->contains('job1'));
assert($run->skip()->contains('job2'));

Implementation details

Conditions are evaluated in the Runner:
src/Runner.php
foreach ($job->runIf() as $runIf) {
    if ($new->getRunIfCondition($runIf) === false) {
        $new->addJobSkip($name);
        return $new;
    }
}

foreach ($job->runIfNot() as $runIfNot) {
    if ($new->getRunIfCondition($runIfNot) === true) {
        $new->addJobSkip($name);
        return $new;
    }
}
The condition evaluation logic:
src/Runner.php
private function getRunIfCondition(
    VariableInterface|ResponseReferenceInterface|callable|bool $runIf
): bool {
    return match (true) {
        is_bool($runIf) => $runIf,
        $runIf instanceof VariableInterface => 
            $this->run->arguments()->required($runIf->__toString())->bool(),
        $runIf instanceof ResponseReferenceInterface => $runIf->key() !== null
            ? $this->run->response($runIf->job())->array()[$runIf->key()]
            : $this->run->response($runIf->job())->bool(),
        default => call_user_func($runIf, $this->run())
    };
}

Conditional duplicate protection

You cannot specify the same condition multiple times:
use OverflowException;

try {
    $closure = fn() => true;
    $job = async(new MyAction())
        ->withRunIf($closure, $closure); // Error!
} catch (OverflowException $e) {
    // Condition `callable#123` is already defined
}

Use cases

$workflow = workflow(
    experimental: async(new ExperimentalFeature())
        ->withRunIf(variable('experimentalEnabled')),
    stable: async(new StableFeature())
        ->withRunIfNot(variable('experimentalEnabled')),
);
$workflow = workflow(
    validate: async(new ValidateData()),
    process: async(new ProcessData())
        ->withRunIf(response('validate', 'isValid')),
    error: async(new LogError())
        ->withRunIfNot(response('validate', 'isValid')),
);
$workflow = workflow(
    checkAuth: async(new AuthCheck()),
    adminTask: async(new AdminAction())
        ->withRunIf(response('checkAuth', 'isAdmin')),
    userTask: async(new UserAction())
        ->withRunIfNot(response('checkAuth', 'isAdmin')),
);
$workflow = workflow(
    debug: async(new DebugLogger())
        ->withRunIf(variable('isDevelopment')),
    analytics: async(new TrackAnalytics())
        ->withRunIfNot(variable('isDevelopment')),
);

Best practices

1

Keep conditions simple

Prefer simple boolean checks over complex callable logic for better readability.
2

Check skip status before accessing responses

Always verify a job wasn’t skipped before accessing its response to avoid exceptions.
3

Use meaningful variable names

Name conditional variables clearly: isEnabled, hasPermission, shouldProcess.
4

Document conditional logic

Add comments explaining why jobs are conditional, especially with complex logic.

Build docs developers (and LLMs) love