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
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-08-28 16:20:26 UTC
README
Simple, plugin-based workflow engine for PHP with branch-first step contracts, context-based data flow, 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
Static data-flow validation (without executing steps) is available through:
WorkflowFlowValidatorInterfaceWorkflowFlowValidatorBuilder
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>] (after config_map resolution) for non-terminal branches.
Workflow definition
A workflow definition is represented by WorkflowDefinitionVo and stores:
- named workflow steps as
PayloadVoentries - an explicit workflow-level
input_schema
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.
Data flow between steps
A step returns only its own output. The engine never forwards that output as the next step input.
The only carrier of data between steps is run context, declared in the workflow definition:
start()writes the workflow payload into run context (not into the first step input)output_mapcopies selected step output fields into contextinput_mapbuilds the next step input from context (and literals)config_mapcan overlay step config from context, including branch targets
Every step that needs data — including the first step — must declare input_map.
Example definition with input_schema, input_map, and output_map:
use AdachSoft\Workflow\Engine\PayloadVo;
use AdachSoft\Workflow\Engine\WorkflowDefinitionVo;
use AdachSoft\Workflow\Enum\SchemaPhaseEnum;
use AdachSoft\Workflow\Schema\PayloadSchemaVo;
$definitionRepository->save(new WorkflowDefinitionVo('example', [
'append' => new PayloadVo([
'type' => 'append_message',
'config' => new PayloadVo([
'message' => 'processed',
'on_next' => 'finish',
]),
'input_map' => new PayloadVo([
'messages' => '$context.messages',
]),
'output_map' => new PayloadVo([
'messages' => '$context.messages',
]),
]),
'finish' => new PayloadVo([
'type' => 'finish',
'config' => PayloadVo::empty(),
'input_map' => new PayloadVo([
'messages' => '$context.messages',
]),
]),
], PayloadSchemaVo::fromArray([
'messages' => [
'type' => 'array',
'required' => true,
'description' => 'Initial messages',
],
], SchemaPhaseEnum::Input)));
Schema Validation
WorkflowEngine and DebugEngine validate payloads in these phases:
- Workflow input (
start()payload vs workflowinput_schema) - strict mode (unknown keys are rejected) - Step input (before execution) - permissive mode (extra keys allowed)
- Step config (before execution) - strict mode
- Step output (after execution, per selected branch) - strict mode
Validation errors raise SchemaValidationException.
Branch contract violations can raise:
UnknownBranchExceptionBranchTargetMissingException
Static flow validation
WorkflowFlowValidatorBuilder analyzes a workflow graph without executing steps. It reports missing required inputs, unknown $context. references, unreachable steps, unknown step types, and related contract issues.
use AdachSoft\Workflow\Validation\WorkflowFlowValidatorBuilder;
$flowValidator = WorkflowFlowValidatorBuilder::create()
->withWorkflowDefinitionRepository($definitionRepository)
->withStepRegistry($stepRegistry)
->build();
$result = $flowValidator->validate('example');
if (!$result->valid) {
foreach ($result->violations as $violation) {
// $violation->stepName, $violation->code, $violation->message
}
}
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\Enum\SchemaPhaseEnum;
use AdachSoft\Workflow\Registry\InMemoryStepRegistry;
use AdachSoft\Workflow\Repository\InMemoryWorkflowDefinitionRepository;
use AdachSoft\Workflow\Repository\InMemoryWorkflowRunRepository;
use AdachSoft\Workflow\Schema\PayloadSchemaVo;
#[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',
]),
'input_map' => new PayloadVo([
'messages' => '$context.messages',
]),
'output_map' => new PayloadVo([
'messages' => '$context.messages',
]),
]),
'finish' => new PayloadVo([
'type' => 'finish',
'config' => PayloadVo::empty(),
'input_map' => new PayloadVo([
'messages' => '$context.messages',
]),
]),
], PayloadSchemaVo::fromArray([
'messages' => ['type' => 'array', 'required' => true, 'description' => 'Initial messages'],
], SchemaPhaseEnum::Input)));
$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();
Create engines only through builders. Do not instantiate WorkflowEngine, DebugEngine, or WorkflowFlowValidator directly.
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
startFrom() writes the supplied payload into run context and does not validate it against workflow input_schema.
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.
- Data between steps travels only through run context and declared maps.
Development
Available Composer scripts:
composer test
composer phpstan
composer cs-fix
composer cs-check