Automatic Transitions
Automatic transitions let the engine branch without an explicit user-triggered event.
What They Do
When the entity enters a state, the engine can immediately evaluate outgoing automatic transitions.
This is useful for:
- routing logic
- conditional branching
- auto-completion flows
In diagrams (and in the generated Mermaid output) automatic transitions are drawn as dashed orange arrows to set them apart from user-triggered ones. The first matching condition wins; an unconditioned transition acts as the fallback:
Defining With Attributes
Mark a transition automatic: true and gate it with a #[Condition] method on the state (mirroring #[Guard]). The method returns bool; the transition is taken only when it returns true:
use Workflow\Attribute\Condition;
use Workflow\Attribute\Transition;
#[Transition(to: ApprovedState::class, name: 'auto_approve', automatic: true)]
class ReviewState extends OrderState
{
#[Condition('auto_approve')]
public function isTrusted(): bool
{
return (bool)$this->getEntity()?->get('trusted');
}
}When the entity enters review, the engine evaluates auto_approve and advances to approved if isTrusted() returns true. Leave the condition off for an unconditional fallback branch. This matches the automatic/condition keys in the NEON/YAML formats.
How Selection Works
The engine evaluates automatic transitions in order:
- the first matching condition wins
- a transition without a condition acts as a fallback branch
To avoid loops, the engine enforces a maximum automatic transition count.
Always Provide a Fallback on a Branch
When a state has several automatic transitions (a branch) that are all conditional and none matches at runtime, the item has nowhere to go and silently stays put — easy to introduce by forgetting the else branch. Guard against it:
bin/cake workflow validatereports any automatic branch state (more than one automatic transition) that has no unconditional fallback. WithWorkflow.strictModeoff this is a warning; with it on it is a hard error (non-zero exit).- With
Workflow.strictModeenabled, the engine throws at runtime instead of staying put when such a branch is reached and no condition matches — turning a silent stuck into a loud, debuggable failure.
The cleanest fix is simply to add an unconditional fallback transition (the else).
Two cases are exempt, because the item is not actually stuck:
- a single conditional automatic transition — a deliberate "advance when ready, otherwise wait" step, so it stays put when its condition is false (even in strict mode);
- a branch state that also has a non-automatic transition — a manual transition, or the transition a timeout fires — since a user or the timeout worker can still move the item.
Only a branch whose automatic transitions are its sole exit is reported or throws.
Important Limitation
Automatic transitions are still part of a single-state model.
They do not mean:
- parallel active states
- fork/join workflow nets
- multiple simultaneous markings
So they help with branching, but they do not turn the engine into a multi-place workflow system.