Search by

adachsoft / workflow

Lightweight, extensible PHP workflow engine with plugin-based steps, context isolation, debugging and nested workflow support

Maintainers

Package info

gitlab.com/a.adach/Workflow

Issues

pkg:composer/adachsoft/workflow

Transparency log

Statistics

Installs: 26

Dependents: 1

Suggesters: 0

Stars: 0

v4.0.0 2026-08-28 18:04 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:

  • WorkflowDefinitionRepositoryInterface
  • WorkflowRunRepositoryInterface
  • StepRegistryInterface

Available engines:

  • WorkflowEngineInterface for synchronous workflow execution
  • DebugEngineInterface for step-by-step workflow debugging

You can build ready-to-use in-memory engines with:

  • WorkflowEngineBuilder
  • DebugEngineBuilder

Static data-flow validation (without executing steps) is available through:

  • WorkflowFlowValidatorInterface
  • WorkflowFlowValidatorBuilder

Main Concepts

Step (branch-first)

A workflow step implements StepInterface and exposes metadata through #[StepType]:

  • inputSchema - expected step input
  • configSchema - expected step config
  • branches - 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 PayloadVo entries
  • 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_map copies selected step output fields into context
  • input_map builds the next step input from context (and literals)
  • config_map can 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 workflow input_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:

  • UnknownBranchException
  • BranchTargetMissingException

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 output
  • SleepStep - 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