adachsoft / workflow
Lightweight, extensible PHP workflow engine with plugin-based steps, context isolation, debugging and nested workflow support
Requires
- php: ^8.2
- adachsoft/collection: ^3.0
Requires (Dev)
- adachsoft/php-code-style: ^0.4.2
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^13.0
- rector/rector: ^2.3
This package is auto-updated.
Last update: 2026-05-21 03:45:37 UTC
README
Simple, plugin-based workflow engine for PHP with branch-first step contracts and built-in payload schema validation.
Requirements
- PHP 8.2+
adachsoft/collection^3.0
Installation
composer require adachsoft/workflow
Overview
The library provides two engines built around three central contracts:
WorkflowDefinitionRepositoryInterfaceWorkflowRunRepositoryInterfaceStepRegistryInterface
Available engines:
WorkflowEngineInterfacefor synchronous workflow executionDebugEngineInterfacefor step-by-step workflow debugging
You can build ready-to-use in-memory engines with:
WorkflowEngineBuilderDebugEngineBuilder
Main Concepts
Step (branch-first)
A workflow step implements StepInterface and exposes metadata through #[StepType]:
inputSchema- expected step inputconfigSchema- expected step configbranches- declared execution branches and output schemas
A step returns StepResultDto::branch('<branch_name>', $output).
The engine resolves the next workflow step from current step config[<branch_name>] for non-terminal branches.
Workflow definition
A workflow definition is represented by WorkflowDefinitionVo and stores named workflow steps as PayloadVo entries.
The first defined key is treated as the entry step.
Workflow run
A workflow run is represented by WorkflowRunVo and stores:
- current step
- input
- output
- finished state
- per-run context (
WorkflowRunContextVo)
Payload
PayloadVo is an immutable value object used for step input, step config, and step output.
Schema Validation
WorkflowEngine validates payloads in three phases:
- Input phase (before execution) - permissive mode (extra keys allowed)
- Config phase (before execution) - strict mode
- Output phase (after execution, per selected branch) - strict mode
Validation errors raise SchemaValidationException.
Branch contract violations can raise:
UnknownBranchExceptionBranchTargetMissingException
Basic Usage
use AdachSoft\Workflow\Attribute\StepType;
use AdachSoft\Workflow\Builder\WorkflowEngineBuilder;
use AdachSoft\Workflow\Contracts\StepInterface;
use AdachSoft\Workflow\Engine\PayloadVo;
use AdachSoft\Workflow\Engine\StepContextDto;
use AdachSoft\Workflow\Engine\StepResultDto;
use AdachSoft\Workflow\Engine\WorkflowDefinitionVo;
use AdachSoft\Workflow\Registry\InMemoryStepRegistry;
use AdachSoft\Workflow\Repository\InMemoryWorkflowDefinitionRepository;
use AdachSoft\Workflow\Repository\InMemoryWorkflowRunRepository;
#[StepType(
type: 'append_message',
description: 'Appends configured message',
inputSchema: [
'messages' => ['type' => 'array', 'required' => true, 'description' => 'Current messages'],
],
configSchema: [
'message' => ['type' => 'string', 'required' => true, 'description' => 'Message to append'],
'on_next' => ['type' => 'string', 'required' => true, 'description' => 'Next step name'],
],
branches: [
'on_next' => [
'description' => 'Continue workflow',
'terminal' => false,
'outputSchema' => [
'messages' => ['type' => 'array', 'required' => true, 'description' => 'Updated messages'],
],
],
],
)]
final class AppendMessageStep implements StepInterface
{
public function execute(StepContextDto $stepContextDto): StepResultDto
{
$messages = $stepContextDto->input->get('messages');
$message = $stepContextDto->config->get('message');
if (!is_array($messages) || !is_string($message)) {
return StepResultDto::branch('on_next', new PayloadVo(['messages' => []]));
}
$messages[] = $message;
return StepResultDto::branch('on_next', new PayloadVo([
'messages' => $messages,
]));
}
}
#[StepType(
type: 'finish',
description: 'Finishes workflow',
inputSchema: [
'messages' => ['type' => 'array', 'required' => true, 'description' => 'Final messages'],
],
branches: [
'end' => [
'description' => 'Finish run',
'terminal' => true,
'outputSchema' => [
'status' => ['type' => 'string', 'required' => true, 'description' => 'Final status'],
'messages' => ['type' => 'array', 'required' => true, 'description' => 'Final messages'],
],
],
],
)]
final class FinishStep implements StepInterface
{
public function execute(StepContextDto $stepContextDto): StepResultDto
{
return StepResultDto::branch('end', new PayloadVo([
'status' => 'done',
'messages' => $stepContextDto->input->get('messages'),
]));
}
}
$stepRegistry = new InMemoryStepRegistry();
$stepRegistry->register(AppendMessageStep::class);
$stepRegistry->register(FinishStep::class);
$definitionRepository = new InMemoryWorkflowDefinitionRepository();
$runRepository = new InMemoryWorkflowRunRepository();
$workflowEngine = WorkflowEngineBuilder::create()
->withStepRegistry($stepRegistry)
->withWorkflowDefinitionRepository($definitionRepository)
->withWorkflowRunRepository($runRepository)
->build();
$definitionRepository->save(new WorkflowDefinitionVo('example', [
'append' => new PayloadVo([
'type' => 'append_message',
'config' => new PayloadVo([
'message' => 'processed',
'on_next' => 'finish',
]),
]),
'finish' => new PayloadVo([
'type' => 'finish',
'config' => PayloadVo::empty(),
]),
]));
$runId = $workflowEngine->start('example', new PayloadVo([
'messages' => ['created'],
]));
$run = $runRepository->get($runId);
Builders
WorkflowEngineBuilder and DebugEngineBuilder provide two usage modes:
- build a default in-memory engine
- build an engine with custom repositories and a custom step registry
Example:
use AdachSoft\Workflow\Builder\DebugEngineBuilder;
$debugEngine = DebugEngineBuilder::create()->build();
Built-in Steps
The package currently provides these built-in steps in src/Steps:
RunWorkflowStep- starts another workflow and forwards workflow outputSleepStep- pauses execution for configured seconds
Builders can register built-in steps for you:
$workflowEngine = WorkflowEngineBuilder::create()
->withStandardSteps()
->build();
Debug Engine
DebugEngineInterface supports:
- starting a workflow in paused mode
- starting from a selected step
- executing one step at a time
- pausing and resuming a run
- reading the current run state
Step Metadata
The registry can expose all registered step definitions via StepRegistryInterface::getAllDefinitions().
This returns StepDefinitionCollection and can be serialized using collection export methods.
Design Notes
- Engines depend on contracts for repositories and the step registry.
- Workflow execution is synchronous.
- Debug engine executes one step at a time.
- Public API uses immutable value objects and collections.
- Step metadata and branch contracts are centralized in
#[StepType]. - Input/config/output validation is integrated into runtime execution.
Development
Available Composer scripts:
composer test
composer phpstan
composer cs-fix
composer cs-check