Core Concepts
A workflow models the lifecycle of a record. This page explains the key concepts you need to understand.
What Is a State Machine?
A state machine tracks where something is in its lifecycle:
- An order starts as
pending, then becomespaid, thenshipped, thencompleted - A support ticket starts as
open, then becomesin_progress, thenresolvedorclosed
At any moment, the record is in exactly one state. Moving between states happens through transitions.
States
A state represents a point in your entity's lifecycle.
[pending] → [paid] → [shipped] → [completed]States have types:
| Type | Meaning |
|---|---|
| Initial | Starting state for new entities (exactly one per workflow) |
| Final | Terminal state - entity lifecycle is done |
| Failed | Error state (implies final) |
| Regular | Any state that isn't initial, final, or failed |
Example with state types:
stateDiagram-v2
[*] --> pending : (initial)
pending --> paid
paid --> shipped
shipped --> completed
pending --> cancelled
completed --> [*] : (final)
cancelled --> [*] : (failed)Transitions
A transition moves an entity from one state to another.
$workflow = $this->workflowRegistry->get($order);
$workflow->apply('pay');
// order.state: pending → paidTransitions have a name (like pay, ship, cancel) that you use in code.
Happy Path
Mark primary transitions as "happy" to highlight the ideal flow:
pending ──(pay)──► paid ──(ship)──► shipped ──(complete)──► completed
│
└──(cancel)──► cancelledThe pay → ship → complete path is the happy path. The cancel transition is an alternative path.
Guards
Guards control whether a transition is allowed.
#[Guard('pay')]
public function ensurePayable(): bool|string
{
if ($this->getEntity()->total <= 0) {
return 'Order total must be positive';
}
return true;
}A guard returns:
true- transition is allowedstring- transition is blocked with this reason
Multiple guards can protect the same transition. All must pass.
Commands
Commands run when a transition succeeds.
#[Command('pay')]
public function capturePayment(): void
{
$this->getEntity()->set('payment_captured', true);
$this->getEntity()->set('paid_at', new DateTime());
}Use commands to:
- Update entity fields
- Trigger side effects (emails, webhooks)
- Set timestamps
Lifecycle Callbacks
States can define callbacks for entering or leaving:
#[OnEnter]
public function notifyCustomer(): void
{
// Runs when entity enters this state
}
#[OnExit]
public function cleanupResources(): void
{
// Runs when entity leaves this state
}Flags
Flags are metadata tags on states for querying:
#[Flag('done')]
#[FinalState]
class CompletedState extends BaseOrderState
{
}Query entities by flag:
$completedOrders = $this->Orders->find('withFlag', flag: 'done')->toArray();Common flags:
done- work is completebillable- can be invoicedrequires_attention- needs review
Putting It Together
A minimal workflow has:
- States - at least one initial and one final
- Transitions - connections between states
- Guards (optional) - protect transitions
- Commands (optional) - side effects on transitions
Using the Workflow Object (Recommended)
The Workflow class provides a clean, entity-centric API:
// Get a workflow for an entity
$workflow = $this->workflowRegistry->get($order);
// Check and apply
if ($workflow->can('pay')) {
$result = $workflow->apply('pay', ['user_id' => $userId]);
if ($result->isSuccess()) {
$this->Orders->save($order);
}
}
// Query state
$workflow->getStateName(); // 'pending'
$workflow->isInState('pending'); // true
$workflow->isInFinalState(); // false
$workflow->getEnabledTransitions(); // ['pay', 'cancel']Using the Behavior
The behavior provides table-level methods via explicit access:
$behavior = $this->Orders->getBehavior('Workflow');
if ($behavior->canTransition($order, 'pay')) {
$result = $behavior->applyTransition($order, 'pay');
if ($result->isSuccess()) {
$this->Orders->save($order);
}
}Both approaches use the same underlying engine, guards, and commands.
Next Steps
- Quick Start - Build your first workflow
- Behavior Integration - Full API reference
- Attributes - Define states with PHP attributes