Error handling
What fails a workflow
An execution ends as Failed when:
- A node returns a failure result (
NodeResult::fail(...)) — for example a required field missing, an Eloquent record not found,fail_on_erroron an HTTP node hitting a 4xx/5xx, division by zero. - A node's
handle()throws and its retry budget is exhausted (or it has none). - A Stop node runs with
outcome: failure.
$execution->error_message carries the reason. Node-level detail is in the
execution log — the failing node's row has status: error (or failed) and its
error field set.
An execution ends as Cancelled (not an error) when a Stop node runs with
outcome: success.
Failures inside a queued run or resume are captured as a Failed execution
record — the job itself doesn't rethrow.
Retries
Any node can be given retry behaviour through its
advanced settings (max_retries,
retry_delay, retry_backoff). Retries only apply when handle() throws —
a returned NodeResult::fail(...) is a deliberate failure and is not retried.
Observing failures
$execution = Workflow::run('flag-large-orders', $payload);
if ($execution?->status === ExecutionStatus::Failed) {
Log::error("Workflow {$execution->workflow_id} failed", [
'execution_id' => $execution->id,
'reason' => $execution->error_message,
]);
}
Or query after the fact:
use Qanna\WorkflowEngine\Models\WorkflowExecution;
WorkflowExecution::where('status', ExecutionStatus::Failed)
->latest('started_at')
->limit(100)
->get();
Hook workflow.finished to react to every terminal outcome centrally — see
Hooks.
Exceptions you might catch
| Exception | Thrown by | When |
|---|---|---|
RuntimeException | Workflow::run() / runDraft() / resume() | Running an unknown workflow id; running a workflow that has never been published; resuming an unknown execution id; resuming a non-suspended execution. |
LogicException | $workflow->publish() | The model has unsaved changes to its content. Save it first. |
Illuminate\Database\Eloquent\ModelNotFoundException | $workflow->rollbackTo() | The version number doesn't exist. |
Workflow::dispatch() doesn't check the workflow up front. If the id is unknown
or the workflow has never been published, the queued job throws in the worker
and fails like any other queued job — no execution record is created.