Skip to content

Execution Flow

This page traces the complete lifecycle of a workflow from dispatch to completion, using an order processing workflow as an example.

Overview

Event Dispatch -> Instance Creation -> Step Loop -> Transition -> Completion

Phase 1: Dispatch

When you call Workflowable::dispatch($event) or Workflowable::dispatchSync($event):

  1. The engine looks up all active Workflow definitions whose event_name matches the event
  2. For each matching workflow, a WorkflowInstance is created with:
    • status: pending
    • current_step: initial_step from the definition
    • workflow_event: the serialized WorkflowEvent DTO
    • state: the event's constructor parameters as key-value pairs
  3. A WorkflowWasCreated domain event is recorded (Spatie event sourcing)
  4. A WorkflowStarted Laravel event is dispatched

For async dispatch, each instance is wrapped in an ExecuteWorkflowJob and pushed to the queue.

Phase 2: Step Execution

The engine enters a loop, executing steps until it hits a terminal state:

  1. Resolve step type - The step's type field is looked up in the StepTypeRegistry
  2. Create StepExecution - A record is created to track this execution attempt
  3. Dispatch StepStarted - Laravel event fired
  4. Execute the handler:
    • action: Resolves the handler class and calls it with the ExecutionContext
    • conditional: Resolves the registered conditional handler and calls evaluate() with the ExecutionContext
    • custom: Delegates to the registered StepType implementation
  5. Record result - Output is merged into instance state, StepCompleted or StepFailed dispatched

ExecutionContext

The handler receives an ExecutionContext that provides:

php
$context->getVariable('orderId');     // Get a single variable
$context->getVariables();             // Get all variables
$context->getInstance();              // Get the WorkflowInstance model
$context->getStepDefinition();        // Get the current step's definition array

Step Results

Handlers return arrays that get merged into the workflow's state:

php
class ProcessOrder
{
    public function handle(ExecutionContext $context): array
    {
        // Business logic...
        return ['processed' => true, 'receipt_id' => 'RCP-123'];
    }
}

After this step, $context->getVariable('processed') returns true in subsequent steps.

Phase 3: Transition

After a step completes:

  1. The engine looks up the transition for the current step
  2. Simple transition: 'process_order' => 'check_amount' always goes to check_amount
  3. Conditional transition: 'check_amount' => ['true' => 'a', 'false' => 'b'] routes based on the step's result
  4. The current_step is updated and a TransitionTriggered event is dispatched
  5. The loop continues with the next step

Phase 4: Completion

The workflow ends when it transitions to a terminal step:

  • 'completed' - Instance status set to completed, WorkflowCompleted event dispatched
  • 'failed' - Instance status set to failed, WorkflowFailed event dispatched

If no valid transition exists, the workflow fails.

Retry Behavior

If a step fails and has a retry policy:

  1. StepRetrying event is dispatched with the attempt number and delay
  2. The step is re-executed after the configured delay
  3. This continues up to max_attempts
  4. If all attempts fail, StepFailed is dispatched with willRetry: false

Safety Limits

The engine enforces a maximum step count per execution run (configurable via execution.max_steps in config, default: 1000) to prevent infinite loops.