The Workflow provides a durable, replay-based execution engine for long-running processes. It allows you to write complex, multi-step logic as code that can withstand system restarts, network failures, and long delays.
- Durability: If the server crashes mid-process, the workflow resumes exactly where it left off.
- Observability: Every step is recorded in the
workflow_historytable. - Reliability: Built-in retries and timeouts for every activity.
- Simplicity: Write complex asynchronous code in a linear, synchronous style.
Workflow is a package that requires installation before use.
php dock package:install Workflow --packagesThis command automatically:
- Run the migration for Workflow tables.
- Register the
WorkflowServiceProvider. - Set up core bindings for the Engine and Runner.
The system provides generators to quickly scaffold workflows and activities.
The system automatically appends the
WorkfloworActivitysuffix to the class name if you omit it. For example,workflow:create OnboardinggeneratesOnboardingWorkflow.
Generates a new workflow class.
# Shared workflow (App/Workflows)
php dock workflow:create Name
# Modular workflow (App/src/Module/Workflows)
php dock workflow:create Name ModuleGenerates a new activity class.
# Shared activity (App/Activities)
php dock activity:create Name
# Modular activity (App/src/Module/Activities)
php dock activity:create Name ModuleDeletes an existing workflow class.
# Shared workflow
php dock workflow:delete Name
# Modular workflow
php dock workflow:delete Name ModuleDeletes an existing activity class.
# Shared activity
php dock activity:delete Name
# Modular activity
php dock activity:delete Name Module| Concept | Description |
|---|---|
| Workflow | The orchestrator. Defines the business logic and sequence of steps. |
| Activity | A single, idempotent unit of work (e.g., ChargeCard, SendEmail). |
| Command | An instruction yielded by a workflow (Activity, Timer, SideEffect). |
| History | The source of truth for replay. Records every "Event" in an instance. |
Anchor supports a flexible architecture for organizing workflows. A hybrid approach based on your application's modularity.
For workflows that belong to a specific business domain, place them within the module's directory. This keeps domain logic encapsulated.
Example Structure:
App/src/Account/Workflows/ResetPasswordWorkflow.phpApp/src/Account/Activities/GenerateResetToken.php
For workflows that coordinate multiple modules or perform generic system tasks, use a central location.
Example Structure:
App/Workflows/SystemCleanupWorkflow.phpApp/Activities/NotifyAdmin.php
Use Modular for 90% of your business logic. Use Shared only when a workflow acts as a "bridge" between two or more disconnected modules.
Implement the Workflow\Contracts\Workflow interface. Use yield to trigger activities or commands.
namespace App\Account\Workflows;
use Generator;
use Workflow\Contracts\Workflow;
use App\Account\Activities\CreateUserRecord;
use App\Account\Activities\SendWelcomeEmail;
class UserOnboardingWorkflow implements Workflow
{
/**
* The main execution logic.
*/
public function execute(array $input): Generator
{
// Step 1: Create the user (returns the result of the activity)
$userId = yield new CreateUserRecord($input);
// Step 2: Send welcome email
yield new SendWelcomeEmail(['user_id' => $userId]);
return "Onboarding complete for user: $userId";
}
/**
* Handle external signals (e.g., manual approval, cancellation).
*/
public function handleSignal(string $signalName, array $payload): void
{
// Logic to update internal workflow state based on external events
}
}Activities perform the actual "heavy lifting." They should be idempotent, as the engine may retry them on failure.
namespace App\Account\Activities;
use Workflow\Contracts\Activity;
use Throwable;
class CreateUserRecord implements Activity
{
// The engine automatically passes constructor data to the handle method
public function __construct(protected array $payload) {}
public function handle(array $payload): array
{
// ... Logic to create user in DB ...
return ['id' => 123, 'status' => 'created'];
}
public function onFailure(string $instanceId, Throwable $e): void
{
// Cleanup logic if the activity fails after all retries
}
public function compensate(string $instanceId, array $originalPayload): void
{
// Saga pattern: Logic to "undo" this activity if the workflow fails later
}
}The system provides several built-in helpers for common workflow needs.
Workflow execution can be paused for minutes, days, or months.
yield minutes(10);
yield days(3);For non-deterministic logic (like generating a random ID or getting the current time), use sideEffect. The result is recorded once and replayed thereafter.
$token = yield sideEffect(fn() => bin2hex(random_bytes(16)));For trivial tasks, you can yield a closure directly.
yield fn() => Log::info("Processing step...");Configure retries, timeouts, and queues per-activity:
use Workflow\Contracts\ActivityOptions;
yield new ProcessPayment($data, ActivityOptions::make()
->withTimeout(120) // Max 120 seconds
->withRetries(5) // Retry up to 5 times
->withRetryDelay(10) // 10 seconds between retries
->onQueue('payments') // Run on a specific queue
);use Workflow\Workflow;
// 1. Run the workflow (creates instance and triggers first step)
$instanceId = Workflow::run(
UserOnboardingWorkflow::class,
['email' => 'user@example.com'], // Input
'user-123' // Business Key (Optional)
);
// 2. Resume or manually trigger execution (done automatically by the engine usually)
Workflow::execute($instanceId);- Step Execution: The engine runs the code until it hits a
yield. - Persistence: The yielded Command is saved as an event. The workflow process terminates.
- External Action: A worker (or timer) eventually completes the task.
- Resumption: The engine re-instantiates the workflow.
- Deterministic Replay: The engine runs the code from line 1. For every
yieldthat was already completed, it simply returns the saved result instead of re-executing. - Continuation: When it reaches the new
yield, it continues the cycle.
Constraint: Because of Replay, workflow code MUST be deterministic. Do not use
rand()ortime()directly inside theexecutemethod; usesideEffect()instead.
- Idempotent Activities: Always assume your activity might run more than once.
- Granular Activities: Break large tasks into small, yieldable steps for better recovery.
- Use Side Effects: Wrap all non-deterministic functions in
sideEffect. - Version Carefully: If you change the code of a running workflow, the replay might fail (non-deterministic change). Use versioning flags for long-running workflows.
- Business Keys: Use meaningful business keys (like
order-456) instead of random UUIDs for easier tracking.