Conditions
Alpha notice: Expression syntax and condition evaluation follow Symfony Expression Language rules. Edge-case behaviour may change between Core tags.
Condition nodes route workflow execution through outgoing transitions. The runtime does not branch on the condition node object itself — it evaluates transitions[].condition against variables from your workflowable model.
Routing authority
When the runtime reaches a type: condition node, it inspects each outgoing transition in definition order:
- Evaluate
transitions[].conditionwhen present (Symfony Expression Language). - Take the first transition whose condition is truthy.
- If none match, follow the transition marked
is_default: true.
The config.expression field on a condition node is optional metadata for editors and documentation. It is not the routing authority. Keep transition conditions aligned with any display expression you store on the node.
// Node — documentation / editor metadata only
[
'key' => 'amount_gate',
'type' => 'condition',
'name' => 'Amount Gate',
'config' => ['expression' => 'refund_amount >= 500'],
],
// Transitions — runtime routing authority
['from' => 'amount_gate', 'to' => 'risk_reviewer_review', 'condition' => 'refund_amount >= 500'],
['from' => 'amount_gate', 'to' => 'end_approved', 'is_default' => true],
Variable source
Expressions read from WorkflowContextInterface::getWorkflowVariables() on the workflowable model:
use DbflowLabs\Core\Contracts\WorkflowContextInterface;
use DbflowLabs\Core\Traits\HasWorkflow;
class RefundDispute extends Model implements WorkflowContextInterface
{
use HasWorkflow;
public function getWorkflowVariables(): array
{
return [
'refund_amount' => (float) $this->refund_amount,
'risk_score' => (int) $this->risk_score,
];
}
}
The engine evaluates conditions immediately when traversing the graph — there is no pending task at a condition node.
Expression engine
DBFlow alpha uses Symfony Expression Language for transitions[].condition strings.
Common patterns that work well in demo workflows:
| Expression | Demo workflow |
|---|---|
refund_amount >= 500 |
refund_dispute_approval |
amount >= 10000 |
procurement_request_approval |
Prefer comparisons on scalars (float, int, string, bool) that you control explicitly.
Safe variable design
Keep condition inputs predictable:
- Expose explicit scalars — return
amount,department,risk_score; do not pass entire Eloquent models into the variable bag. - Cast numeric fields — use
(float) $this->amountso string columns do not surprise comparisons. - Keep conditions deterministic — avoid time-dependent values, randomness, or external API calls inside
getWorkflowVariables(). - Name transitions after business rules — when a threshold changes, update both the transition
conditionand any optionalconfig.expressionon the node. - Validate definitions in CI — run
WorkflowDefinitionValidator::validateOrFail($definition)so malformed graphs fail in PHPUnit, not in production.
// Avoid — opaque object references inside expressions
public function getWorkflowVariables(): array
{
return ['request' => $this]; // do not do this
}
// Prefer — explicit, typed scalars
public function getWorkflowVariables(): array
{
return [
'amount' => (float) $this->amount,
'department' => (string) $this->department,
];
}
Default transitions
Every condition node should expose exactly one default outgoing transition (is_default: true). The default path runs when no conditional transition matches.
In the refund demo, amounts below the threshold skip risk_reviewer_review and flow directly to end_approved.
Testing conditions
Branch coverage belongs in integration tests. The dbflow-demo project includes:
tests/Feature/RefundDisputeAmountBranchingTest.php— high vs lowrefund_amountpathstests/Feature/ProcurementRequestDbflowIntegrationTest.php— procurement amount gate
See Testing Workflows for PHPUnit patterns.
What's next
- Refund Approval → —
refund_amount >= 500walkthrough - Purchase Request Approval → —
amount >= 10000escalation - Code-defined Workflows → — full JSON schema and node types
- Eloquent Models → —
WorkflowContextInterfaceintegration