solution-forest/workflow-engine-core
Framework-agnostic workflow engine core for PHP 8.3+. Define and run workflows with type-safe steps, state tracking/persistence, plugins for actions/storage, retries, timeouts, and rich error handling. Actively developed; not production-ready.
A powerful, framework-agnostic workflow engine for PHP applications. This core library provides comprehensive workflow definition, execution, and state management capabilities without any framework dependencies.
โ ๏ธ WARNING: DEVELOPMENT STATUSโ ๏ธ
This package is currently under active development and is NOT READY FOR PRODUCTION USE.
Features may be incomplete, APIs might change, and there could be breaking changes. Use at your own risk in development environments only.
composer require solution-forest/workflow-engine-core
# Clone the repository
git clone https://github.com/solution-forest/workflow-engine-core.git
cd workflow-engine-core
# Install dependencies
composer install
# Run quality checks
composer ci
use SolutionForest\WorkflowEngine\Core\WorkflowBuilder;
use SolutionForest\WorkflowEngine\Core\WorkflowEngine;
use SolutionForest\WorkflowEngine\Core\WorkflowContext;
use SolutionForest\WorkflowEngine\Core\ActionResult;
use SolutionForest\WorkflowEngine\Actions\BaseAction;
// Define custom actions
class ValidateOrderAction extends BaseAction
{
public function execute(WorkflowContext $context): ActionResult
{
$orderId = $context->getData('order_id');
// Your validation logic here
if ($this->isValidOrder($orderId)) {
return ActionResult::success(['validated' => true]);
}
return ActionResult::failure('Invalid order');
}
}
// Build a workflow definition
$definition = WorkflowBuilder::create('order-processing')
->description('Process customer orders')
->addStep('validate', ValidateOrderAction::class)
->addStep('payment', ProcessPaymentAction::class, timeout: 300, retryAttempts: 3)
->addStep('fulfillment', FulfillOrderAction::class)
->build();
// Create engine with storage adapter and event dispatcher
$engine = new WorkflowEngine($storageAdapter, $eventDispatcher);
// Start and run the workflow
$instanceId = $engine->start(
'order-processing',
$definition->toArray(),
['order_id' => 123, 'customer_id' => 456]
);
// Check the result
$instance = $engine->getInstance($instanceId);
echo $instance->getState()->value; // "completed"
use SolutionForest\WorkflowEngine\Core\WorkflowBuilder;
$workflow = WorkflowBuilder::create('order-flow')
->description('Process customer orders')
->addStep('validate', ValidateOrderAction::class)
->when('order.total > 1000', function ($builder) {
$builder->addStep('fraud_check', FraudCheckAction::class);
})
->addStep('payment', ProcessPaymentAction::class, timeout: 300, retryAttempts: 3)
->email('order-confirmation', 'customer@example.com', 'Order Confirmed')
->build();
// Quick templates for common patterns
$workflow = WorkflowBuilder::quick()->userOnboarding();
$workflow = WorkflowBuilder::quick()->orderProcessing();
$workflow = WorkflowBuilder::quick()->documentApproval();
Note: Attributes are currently metadata annotations for documentation and tooling. They are not yet auto-parsed by the engine at runtime โ use the builder API (
timeout:,retryAttempts:,when()) or step config to apply these behaviors. Attribute-driven execution is planned for a future release.
Use native PHP attributes to annotate actions with retry, timeout, and conditions:
use SolutionForest\WorkflowEngine\Attributes\Retry;
#[Retry(attempts: 3, backoff: 'exponential', delay: 1000)]
class ReliableApiAction extends BaseAction
{
public function execute(WorkflowContext $context): ActionResult
{
// Retries up to 3 times with exponential backoff starting at 1s
return ActionResult::success();
}
}
use SolutionForest\WorkflowEngine\Attributes\Timeout;
#[Timeout(seconds: 30)]
class TimedAction extends BaseAction
{
public function execute(WorkflowContext $context): ActionResult
{
// Will timeout after 30 seconds
return ActionResult::success();
}
}
use SolutionForest\WorkflowEngine\Attributes\Condition;
#[Condition('order.amount > 100')]
class PremiumProcessingAction extends BaseAction
{
public function execute(WorkflowContext $context): ActionResult
{
// Only executes when order.amount > 100
return ActionResult::success();
}
}
use SolutionForest\WorkflowEngine\Attributes\WorkflowStep;
#[WorkflowStep(id: 'send_email', name: 'Send Welcome Email', description: 'Sends a welcome email to the new user')]
class SendWelcomeEmailAction extends BaseAction
{
public function execute(WorkflowContext $context): ActionResult
{
return ActionResult::success();
}
}
These features can also be configured through the fluent builder API:
// Steps with retry and timeout configured via builder
$workflow = WorkflowBuilder::create('reliable-flow')
->addStep('fetch_data', FetchDataAction::class, timeout: 30, retryAttempts: 3)
->addStep('process', ProcessAction::class, timeout: 60)
->build();
// Conditional steps evaluated at runtime
$workflow = WorkflowBuilder::create('conditional-flow')
->addStep('validate', ValidateAction::class)
->when('order.total > 1000', function ($builder) {
$builder->addStep('fraud_check', FraudCheckAction::class);
})
->addStep('complete', CompleteAction::class)
->build();
// Start, resume, cancel
$instanceId = $engine->start('my-workflow', $definition->toArray(), ['key' => 'value']);
$instance = $engine->getInstance($instanceId);
$engine->resume($instanceId);
$engine->cancel($instanceId, 'No longer needed');
// Track progress
$progress = $instance->getProgress(); // 0.0 to 100.0
$summary = $instance->getStatusSummary();
// Query instances with filters
$instances = $engine->getInstances([
'state' => 'running',
'definition_name' => 'order-processing',
'created_after' => new \DateTime('-7 days'),
'limit' => 50,
'offset' => 0,
]);
For quick workflow execution without manual engine setup:
use SolutionForest\WorkflowEngine\Support\SimpleWorkflow;
$simple = new SimpleWorkflow($storageAdapter);
// Run actions sequentially
$instanceId = $simple->sequential('user-onboarding', [
SendWelcomeEmailAction::class,
CreateUserProfileAction::class,
AssignDefaultRoleAction::class,
], ['user_id' => 123]);
// Run a single action as a workflow
$instanceId = $simple->runAction(SendEmailAction::class, [
'to' => 'user@example.com',
'subject' => 'Welcome!',
]);
// Execute from a builder
$builder = WorkflowBuilder::create('custom-flow')
->addStep('validate', ValidateAction::class)
->addStep('process', ProcessAction::class);
$instanceId = $simple->executeBuilder($builder, $context);
// Check status
$status = $simple->getStatus($instanceId);
// Returns: id, state, current_step, progress, completed_steps, failed_steps, error_message, ...
The workflow engine follows a clean architecture with clear separation of concerns:
WorkflowBuilder โ WorkflowDefinition โ WorkflowEngine โ Executor โ Actions
โ
StateManager โ StorageAdapter
โ
EventDispatcher
| Component | Purpose |
|---|---|
| WorkflowBuilder | Fluent API for constructing workflow definitions with addStep(), when(), email(), delay(), http() |
| WorkflowDefinition | Immutable blueprint containing steps, transitions, conditions, and metadata |
| WorkflowEngine | Central orchestrator โ start(), resume(), cancel(), getInstance(), getInstances(), getStatus() |
| Executor | Runs steps sequentially with retry logic, timeout enforcement, and condition evaluation |
| StateManager | Coordinates persistence through StorageAdapter |
| EventDispatcher | Broadcasts 7 event types during workflow lifecycle |
PENDING โ RUNNING โ COMPLETED
โ โ โ
FAILED WAITING
โ โ โ
FAILED โ PAUSED
โ
CANCELLED โ (any non-terminal state)
Valid transitions:
PENDING โ RUNNING, FAILED, CANCELLEDRUNNING โ WAITING, PAUSED, COMPLETED, FAILED, CANCELLEDWAITING โ RUNNING, FAILED, CANCELLEDPAUSED โ RUNNING, FAILED, CANCELLEDCOMPLETED, FAILED, CANCELLED) โ no further transitionsState transitions are validated at runtime โ invalid transitions throw InvalidWorkflowStateException.
| Namespace | Contents |
|---|---|
Core\ |
WorkflowEngine, WorkflowBuilder, Executor, StateManager, WorkflowInstance, WorkflowDefinition, WorkflowContext, ActionResult, Step, DefinitionParser, ActionResolver |
Actions\ |
BaseAction, LogAction, EmailAction, HttpAction, DelayAction, ConditionAction |
Contracts\ |
WorkflowAction, StorageAdapter, EventDispatcher, Logger |
Attributes\ |
WorkflowStep, Retry, Timeout, Condition |
Events\ |
WorkflowStartedEvent, WorkflowCompletedEvent, WorkflowFailedEvent, WorkflowCancelledEvent, StepCompletedEvent, StepFailedEvent, StepRetriedEvent |
Exceptions\ |
WorkflowException, InvalidWorkflowDefinitionException, InvalidWorkflowStateException, ActionNotFoundException, StepExecutionException, WorkflowInstanceNotFoundException |
Support\ |
NullLogger, NullEventDispatcher, SimpleWorkflow, Uuid, Timeout, ConditionEvaluator, Arr |
Six ready-to-use actions are included:
| Action | Purpose | Config Keys |
|---|---|---|
| LogAction | Log messages with placeholder replacement ({user.name}) |
message, level (debug/info/warning/error) |
| EmailAction | Mock email sending with template support | to, subject, body, template |
| HttpAction | HTTP requests with {{ variable }} template variables |
url, method, headers, body |
| DelayAction | Pause execution for a specified duration | seconds, minutes, hours |
| ConditionAction | Evaluate boolean expressions and branch (on_true/on_false) |
condition, on_true, on_false |
| BaseAction | Abstract base class for custom actions | โ |
The WorkflowState enum provides utility methods for UI and logic:
$state = $instance->getState();
$state->isActive(); // true for PENDING, RUNNING, WAITING, PAUSED
$state->isFinished(); // true for COMPLETED, FAILED, CANCELLED
$state->isSuccessful(); // true for COMPLETED
$state->isError(); // true for FAILED
$state->label(); // "Running"
$state->description(); // "The workflow is actively executing steps..."
$state->color(); // "blue" (gray, blue, yellow, orange, green, red, purple)
$state->icon(); // "โถ๏ธ"
$state->canTransitionTo(WorkflowState::COMPLETED); // bool
$state->getValidTransitions(); // [WorkflowState::WAITING, ...]
Implement the StorageAdapter interface for custom persistence:
use SolutionForest\WorkflowEngine\Contracts\StorageAdapter;
class DatabaseStorageAdapter implements StorageAdapter
{
public function save(WorkflowInstance $instance): void { /* ... */ }
public function load(string $id): WorkflowInstance { /* ... */ }
public function findInstances(array $criteria = []): array { /* ... */ }
public function delete(string $id): void { /* ... */ }
public function exists(string $id): bool { /* ... */ }
public function updateState(string $id, array $updates): void { /* ... */ }
}
Listen to workflow events โ 7 event types are dispatched during execution:
use SolutionForest\WorkflowEngine\Contracts\EventDispatcher;
use SolutionForest\WorkflowEngine\Events\WorkflowStartedEvent;
use SolutionForest\WorkflowEngine\Events\WorkflowCompletedEvent;
use SolutionForest\WorkflowEngine\Events\WorkflowFailedEvent;
use SolutionForest\WorkflowEngine\Events\WorkflowCancelledEvent;
use SolutionForest\WorkflowEngine\Events\StepCompletedEvent;
use SolutionForest\WorkflowEngine\Events\StepFailedEvent;
use SolutionForest\WorkflowEngine\Events\StepRetriedEvent;
class CustomEventDispatcher implements EventDispatcher
{
public function dispatch(object $event): void
{
match ($event::class) {
WorkflowStartedEvent::class => $this->onWorkflowStarted($event),
WorkflowCompletedEvent::class => $this->onWorkflowCompleted($event),
WorkflowFailedEvent::class => $this->onWorkflowFailed($event),
StepCompletedEvent::class => $this->onStepCompleted($event),
StepFailedEvent::class => $this->onStepFailed($event),
StepRetriedEvent::class => $this->onStepRetried($event),
default => null,
};
}
}
Provide a custom logging implementation (PSR-3 style):
use SolutionForest\WorkflowEngine\Contracts\Logger;
class CustomLogger implements Logger
{
public function info(string $message, array $context = []): void { /* ... */ }
public function warning(string $message, array $context = []): void { /* ... */ }
public function error(string $message, array $context = []): void { /* ... */ }
public function debug(string $message, array $context = []): void { /* ... */ }
}
Run the test suite:
# Run all tests
composer test
# Run tests with coverage
composer test:coverage
# Run specific test file
vendor/bin/pest tests/Unit/WorkflowEngineTest.php
# Run tests with detailed output
vendor/bin/pest --verbose
We use several tools to maintain high code quality:
# Static analysis with PHPStan
composer analyze
# Code formatting with Laravel Pint
composer pint
# Check code formatting without making changes
composer pint --test
# Run all quality checks
composer pint && composer analyze && composer test
phpstan.neon.dist - PHPStan configuration for static analysispint.json - Laravel Pint configuration for code formattingphpunit.xml.dist - PHPUnit configuration for testing.github/workflows/run-tests.yml - CI/CD pipeline configurationWe maintain high code quality through:
This core library is framework-agnostic. For specific framework integrations:
solution-forest/workflow-engine-laravelWe welcome contributions! Please see CONTRIBUTING.md for details.
This project is licensed under the MIT License - see the LICENSE file for details.
How can I help you explore Laravel packages today?