Troubleshooting
Start with php artisan workflows:doctor (add --json in CI, --strict to fail on warnings), then workflows:list --status=failed and workflows:show <ulid> for one instance’s path, step log, waits and next escalation.
Doctor findings
Levels are the DoctorLevel enum (ok, warn, fail):
| Check | Level | Finding | Fix |
|---|---|---|---|
| key types | ok / fail | A registered subject’s key doesn’t fit subject_key_type. | Set workflows.subject_key_type (or key_type) to the model’s key type before migrating. |
| owner key type | fail | The teams model’s key doesn’t fit owner_key_type. | Set workflows.owner_key_type to uuid / ulid. |
| queue | warn | Queue off, or the connection is sync. | Fine for local work; in production turn the queue on with a real connection. |
| queue timeout | fail | retry_after ≤ the step job timeout. | Raise the connection’s retry_after above engine.lease − 30 s, or lower engine.lease. |
| pcntl | warn | The worker can’t enforce the job timeout. | Install the pcntl extension on workers. |
| cache | warn | The default cache store is array or file. | Use a store every server shares (redis, database, memcached, dynamodb) for onOneServer(). |
| wake | warn | workflows:wake never ran, or last ran more than five minutes ago. | Schedule it every minute with withoutOverlapping()->onOneServer() and run the scheduler. |
| step types | ok / fail | The catalogue fails to load, or an active version uses a step type that is no longer registered. | Fix or remove the named class, register the type again, or publish a version without it. |
| config schema | fail | A host type’s configSchema() uses a keyword the validator doesn’t support. | Write it with ConfigSchema. |
| adapter | fail | An active definition uses a step type whose package is missing. | composer require roundly-consulting/<package>, or publish a version without the step. |
| calendar | fail | An active definition’s default calendar is missing. | Register it, or create the opening-hours calendar for the owner. |
| opening hours | fail | An active definition uses an opening_hours: calendar but its owner model doesn’t use HasOpeningHours. | Add implements OpeningHoursOwner + use HasOpeningHours to the owner model (for teams: your swapped team model). |
| condition waits | warn | An active version waits on a condition of a subject type whose model doesn’t use HasWorkflows. | Add the trait, or accept that it re-checks on its poll only. |
| events | fail / warn | A mapped class doesn’t exist or '*' isn’t a WorkflowEvent (fail); a mapped event carries no idempotency key, a class is mapped twice, or an on_behalf_of event definition’s mapping has no user (warn). | Fix the mapping; add idempotency_key; keep one mapping; map user or run as system. |
| webhooks | ok / fail | The route is enabled without verify_middleware (it stays unregistered). | Set workflows.webhooks.verify_middleware, e.g. ['sentinel.signed:<profile>']. |
| stuck | warn | Running instances with nothing queued, running or waiting for longer than engine.lease. | workflows:show <ulid>; usually a lost job that wake re-pushes after engine.requeue_after; otherwise workflows:retry or workflows:cancel. |
| wake backlog | warn | Waits or escalations due more than five minutes ago. | Check the scheduler and raise wake.batch. |
| bridges / failures | ok | The installed bridges; how many instances failed in the last 24 hours. | — |
Failure codes
WorkflowInstance::failure() and StepRun::error() return the FailureCode case (a code:detail code by its prefix; FailureCode::detail() reads the detail) or, for a host step’s own code, the string. “Retry helps” means workflows:retry (or instances()->retry()) can succeed once the cause is gone — FailureCode::retryable(). The retry picks up where the step failed: a failed handle() runs again, and a failed resume() is resumed again with the signal it received (the event’s payload, the decision, the timeout), so an event that already arrived does not have to be sent again. Every code has an English and a Slovak message (FailureCode::message()).
| Code | Written when | Retry helps |
|---|---|---|
adapter_missing | A step needs a package that isn’t installed (approval, notify, transition, a form trigger). | yes, after installing it |
approval_cancelled | The approval round was cancelled outside the workflow. | no |
approval_closed | The approval round was already closed when the step acted on it. | no |
approval_missing | The approval round no longer exists. | no |
authorization_denied | The authorization hook refused the step (policy, abilities, the system actor). | yes, after granting it |
calendar_missing | A business calendar the step needs is missing for the owner. | yes, after creating it |
calendar_never_open | The calendar has no open time within the search window. | no (fix the calendar, then cancel and restart) |
child_not_started | A start_workflow child could not be started (retired, inactive, inputs not covered), or the start-rate limit refused it (retried automatically with backoff). | yes, after fixing the child definition (or once the rate window has room) |
delay_field_empty | A delay with until_field found the field empty. | yes, after filling the field |
ended_by_graph | An end node with status: failed. | no |
exception | A handler threw an unknown exception (its message is stored for operators, never rendered). | yes |
field_write_refused | set_field targeted a lifecycle state column. | no |
idempotency_conflict | The lifecycle saw the same idempotency key for a different transition. | no |
invalid_approval_request | Approvals refused the request (configuration). | no |
invalid_output | A handler returned output that isn’t plain JSON or is over steps_output.max_bytes. | no (fix the handler) |
invalid_value | set_field got a value of the wrong type at run time. | no |
lease_expired | A claimed step outlived its lease (a crashed or slow worker). | yes (retried automatically) |
missing_edge | The next node does not exist. | no |
missing_outcome_edge | A step ended with an outcome that has no edge. | no (publish a version with the edge) |
missing_trigger | The version has no trigger. | no |
nesting_depth_exceeded | start_workflow went deeper than limits.max_depth. | no |
no_approvers | The approvers resolved to no one. | yes, after fixing the parties |
no_result | A handler returned nothing. | no (fix the handler) |
notification_refused:<code> | Notifications refused the notification permanently. | no |
owner_mismatch | The subject no longer belongs to the workflow’s owner. | yes, if it moves back |
resume_unsupported | A signal reached a step type that cannot resume. | no |
roles_unavailable | A role party but no roles resolver (teams or parties.roles). | yes, after configuring one |
start_rate_exceeded | The definition started more than limits.max_starts_per_minute times in a minute. | no — no step ran; send the start again (its key was left free) |
step_limit_exceeded | The instance ran more than limits.max_steps steps. | no |
step_missing | An escalation’s waiting step no longer exists. | no |
subject_missing | The subject was deleted or is invisible in the owner context. | yes, if it is restored |
transition_conflict | Another change reached the subject first (retried automatically). | yes |
transition_denied:<code> | The lifecycle guard refused the transition (retried automatically only when the lifecycle says so). | no — unless what the guard checks changes |
transition_unusable | The transition can’t be used on this subject. | no |
unknown_approval_preset | The approval preset does not exist. | no |
unknown_escalation_action | An escalation action type is not available. | no |
unknown_step_type | The node’s step type is not registered (or the node is gone). | yes, after registering it |
version_missing | The instance’s version no longer exists. | no |
worker_timeout | The queue worker killed the job. | yes (retried automatically) |
Package exceptions that a step turns into a failure (a child start refused, for example) keep their own error codes — see Exceptions.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.