Execution & modes
Starting an execution
All three entry points go through the same manager
(ExecutionManagerContract, fronted by the Workflow facade):
use Qanna\WorkflowEngine\Facades\Workflow;
// Run now, in this process. Returns a WorkflowExecution.
$execution = Workflow::run('send-welcome-email', ['email' => 'ada@example.com']);
// Queue it. Returns nothing; a worker runs it later.
Workflow::dispatch('send-welcome-email', ['email' => 'ada@example.com']);
// Resume a suspended execution (see Suspend & resume).
Workflow::resume($executionId, ['approved' => true]);
run() also accepts $triggeredBy (a free-text label recorded on the execution,
default 'manual'), an ExecutionMode, and an optional $executionId.
When an execution starts, the engine:
- Fires the workflow's trigger against the payload. If the trigger returns
TriggerResult::ignore(), no execution is created andrun()returnsnull. - Captures a snapshot of the definition into a new execution record.
- Walks the node graph from the
from: 'trigger'edge until the workflow completes, fails, is stopped, or suspends.
Execution modes
| Mode | Behaviour |
|---|---|
ExecutionMode::Sync | Everything runs in the current process. A short Wait (within sync_wait_timeout) sleeps in-process and resumes immediately; a longer one suspends and returns a Suspended execution. This is the default for Workflow::run(). |
ExecutionMode::Async | The default for queued execution. Wait and child-workflow calls always go through the queue. |
ExecutionMode::Test | Runs the real engine but resolves everything inline and instantly — waits don't sleep, child workflows never touch a real queue. Used by Workflow::fake(). |
The WorkflowExecution record
Every run produces a WorkflowExecution (returned by run() / resume(), and
retrievable later from the execution repository). It is an immutable value object:
$execution->id; // uuid
$execution->workflowId;
$execution->workflowVersion;
$execution->status; // ExecutionStatus enum
$execution->input; // the trigger payload
$execution->output; // final output (see below)
$execution->errorMessage; // set when status is Failed
$execution->logs; // Collection<ExecutionLogEntry>
$execution->meta; // includes 'triggered_by', 'duration_ms'
$execution->startedAt;
$execution->finishedAt;
$execution->durationMs();
$execution->isTerminal(); // Succeeded | Failed | Cancelled
$execution->timeline(); // one summary row per node, in traversal order
Status values
| Status | Meaning |
|---|---|
Running | In progress. |
Succeeded | Reached the end of the graph without error. |
Suspended | Paused, waiting to be resumed (a Wait, a child workflow, a webhook, or a manual resume). Not terminal. |
Failed | A node returned a failure or threw, or a Stop node terminated it as a failure. |
Cancelled | A Stop node ended it intentionally on success. |
Pending | Reserved; not currently produced by the engine. |
What output contains
By default the execution's output is the last node that ran. That is often not what you want once side-effecting nodes run after the "real" result was produced. Put a Set output node at the end of a flow to make the execution return exactly what you choose.
The execution log
Each node writes a running row when it starts and a terminal row
(success / error / failed / waiting / stopped) when it finishes, plus
retrying and warning rows where relevant. $execution->timeline() collapses
these to one row per node with summed durations. Logging can be turned off with
the enable_logger config key.
Read logs later through the execution repository:
use Qanna\WorkflowEngine\Storage\Contracts\ExecutionRepositoryContract;
$repo = app(ExecutionRepositoryContract::class);
$repo->find($executionId);
$repo->listForWorkflow('send-welcome-email', limit: 50);
$repo->listByStatus(ExecutionStatus::Failed);
$repo->logsForExecution($executionId);
$repo->purgeOlderThan(now()->subMonths(3));
Queued execution
Workflow::dispatch() pushes an ExecuteWorkflowJob. Resuming after a long wait
pushes a ResumeExecutionJob (delayed until the wait elapses). Both need a
running queue worker. Failures inside a job surface as a Failed execution
record, not a thrown job exception.