Running transitions
Transitions run in the panel, in code and through the API – always with the same checks.
What the engine checks
Section titled “What the engine checks”When a transition runs, in one transaction:
- The workflow is active in the tenant (
inactive). - The record’s workflow row is locked, so two people cannot move it at the same time.
- The transition exists in the record’s version and starts in its state
(
unknown_transition,wrong_state). Optional transitions switched off by the tenant count as unknown. - The current user or API client matches an actor – or may
override (
not_allowed). - Four eyes (
four_eyes) and guards (guard). - The reason (required, at most 2000 characters) and the form (
validation). - The state changes, the history and the audit entry are written.
- The tasks of the old state are closed, those of the new state created.
- The effects run.
If any step fails, nothing is stored.
The detail page of a record shows, per workflow, the state, the transitions the user may run now, the tasks of the record and the history (see Lists and detail page).
- Transitions with a reason or a form open a dialog,
confirm()asks first. - Only transitions the engine offers are shown; everything is checked again when one runs.
- After the transition a message names the new state.
$engine = service('workflows');$workflow = $resource->workflow('order-approval');
$engine->transitions($resource, $workflow, $record); // what the current user may run$engine->apply($resource, $workflow, $record, 'approve'); // returns the new state, throws WorkflowException$engine->apply($resource, $workflow, $record, 'order', ['order_number' => 'PO-1'], 'Urgent');$engine->runAsSystem($resource, $workflow, $record, 'reject', 'Imported as rejected', 'import');$engine->state($resource, $workflow, $record); // null when the workflow is not active$engine->history($resource, $workflow, $record); // transitions, oldest first$engine->isLocked($resource, $record);| Method | Purpose |
|---|---|
transitions() |
the transitions the current actor may run: name, label, target state, reason (none, optional, required), form, confirm, override |
apply($resource, $workflow, $record, $name, $input = [], $reason = null) |
runs a transition as the current user; $input holds the form values in input format |
runAsSystem($resource, $workflow, $record, $name, $reason, $label) |
runs as the system: no actors and no four eyes; $label names the system actor in the history |
state(), history(), isLocked() |
read the state |
isActive($workflow) |
whether the workflow is active in the current tenant |
Run code in the tenant of the record (tenantContext()->runAs()) in
commands and jobs.
Refusals
Section titled “Refusals”Anvil\Workflow\WorkflowException::$reason names the refusal:
| Reason | Meaning |
|---|---|
inactive |
the workflow is not active in the tenant |
unknown_transition |
no such transition (or switched off) |
wrong_state |
the record is not in a starting state of the transition |
not_allowed |
not an actor and no override permission |
four_eyes |
the actor ran an excluded earlier transition or created the record |
guard |
a guard failed |
validation |
reason or form invalid; $errors holds the messages |
conflict |
the record changed at the same time |
configuration |
the definition or configuration is broken |
With 'transitions' in Resource::$api (and the view permission of the
resource):
| Method and path | Purpose |
|---|---|
GET /api/v1/orders/{uuid}/transitions |
state and transitions the client may run, per workflow |
POST /api/v1/orders/{uuid}/transitions/{name} |
runs a transition; body: reason and the form fields; answers with the new state and the record |
The state column is part of the representation (read only); for
workflows with durations also <column>_due_at, <column>_forecast_at
and <column>_risk. API clients
match permission actors, and the creator actor for records they created;
four eyes compares clients like users. Refusals: 403 (actor, four eyes,
guard), 409 (state, inactive), 422 (reason, form). See
API for machine clients.
New records
Section titled “New records”Records created in the panel or through the API start in the initial state of every active workflow, with their creator. Records created elsewhere (e.g. an import) start on their first transition: a known state in the state column is kept, otherwise the initial state applies; their creator is unknown.