Skip to content

Transitions

Transition::make('approve')->label('Approve')
->from('submitted')->to('approved')
->by(Actor::slot('approver', default: Actor::resolver('line-manager')))
->fourEyes('submit')
->guard('order.within-budget')
->effect('order.notify-requester'),

Names: start with a lower case letter, then letters and digits (camelCase), up to 40 characters. Unique among the workflows of a resource; the API uses them in paths.

Method Meaning
label() text of the button and in the history
from(...), to() states; several starting states are allowed
by(Actor ...) who may run it; one matching actor is enough, see Actors
guard(key or closure) condition on the record; several guards must all pass
fourEyes('submit', 'creator') the actor may not be the actor of these earlier transitions of the record, or its creator
reason(), reason(required: true) asks for an optional or a required reason (at most 2000 characters)
form([...fields]) fields asked when the transition runs
effect(key or closure) runs after the state changed; several effects run in order
confirm('…') confirmation question before running
optional() tenants can switch the transition off
inbox(false) its actors do not get the decision task
automatic(TaskCondition) runs by itself when the tasks of the state meet the condition
->guard('order.has-items') // a building block
->guard(fn (array|object $order, WorkflowContext $context): bool => $order['amount'] > 0)

A transition whose guard fails is not offered, and refused when it runs (guard). Guards decide by the record, not by the user – that is the job of actors.

->fourEyes('submit') // not the person who submitted
->fourEyes('submit', 'creator') // nor the creator of the record

The current actor may not be the actor of the named earlier transitions of this record (their last run), or – with creator – the creator. Four eyes applies to overrides as well. API clients are compared like users.

reason() asks for an optional reason, reason(required: true) for a required one. It is stored in the history and in the audit entry. Running a transition as substitute always requires a reason.

->form([TextInput::make('order_number')->required()->maxLength(30)])
->form(static fn (): array => [DateInput::make('delivery_on')->required()]) // built when shown
->form('order.delivery') // a building block

The fields are asked in a dialog, validated like every form and stored with the transition in the history. All field types work. The converted values reach the effects as $data. A form built in a closure can use lang() for its labels.

->effect('order.send-to-supplier')
->effect(fn (array|object $order, array $data, WorkflowContext $context) => service('orders')->send($order, $data['order_number']))

Effects run after the state changed and the tasks were updated, in the same transaction: if an effect throws, the transition is rolled back. They receive the record as it is now, the form data and the context. Long running work and calls to other systems belong into a queued job or an outbox started by the effect.

optional() lets tenants switch the transition off in their configuration, e.g. cancel for tenants who don’t allow withdrawals. Switched-off transitions are not offered, don’t count for decisions and cannot be run by deadlines.

Transition::make('accept')->from('in_review')->to('approved')->automatic(TaskCondition::all('approved')),

runs by itself as the system when the tasks of the state meet the condition, see Tasks in parallel.

Guards, effects and resolvers receive a WorkflowContext with workflow, blueprint, resource, tenantId, configuration (the released configuration of the tenant) and transition.