All packages
Workflows for Laravel
Conditions
Conditions are declarative JSON. There is no expression language, no code and no regex from users: a fixed interpreter walks the parsed tree and reads registered fields only.
{ "all": [ // all = AND, any = OR, not = negation; nest freely
{ "field": "subject.amount", "op": "gt", "value": "50000" },
{ "any": [
{ "field": "subject.department", "op": "in", "value": ["legal", "finance"] },
{ "field": "input.urgent", "op": "equals", "value": true } ] },
{ "field": "subject.due_on", "op": "before", "value": { "relative": "P7D" } }, // now + 7 days
{ "field": "owner.plan", "op": "equals", "value": { "ref": "trigger.plan" } }
] }- Left operand: field — a value path (subject.*, owner.*, input.*, trigger.*, steps.<id>.output.*).
- Right operand: a literal, {"ref": path}, {"relative": duration} (dates only, relative to now), or [low, high] for between.
- Operators: equals, in, gt, lt, between (inclusive), empty, contains, before, after.
The same in the graph builder:
use RoundlyConsulting\Workflows\Graph\Build\{Ref, When};
When::all(
When::field('subject.amount')->gt('50000'),
When::any(
When::field('subject.department')->in(['legal', 'finance']),
When::field('input.urgent')->equals(true),
),
When::field('subject.title')->contains('laptop', insensitive: true),
When::not(When::field('subject.note')->isEmpty()),
When::field('owner.plan')->equals(Ref::trigger('plan')),
);Operators per field type
| Field type | Allowed operators |
|---|---|
| string, text | equals, in, empty, contains (case-sensitive; add "case": "insensitive") |
| integer, decimal | equals, in, gt, lt, between, empty |
| boolean | equals, empty |
| date, datetime | equals, before, after, between, empty |
| enum | equals, in, empty |
| party | equals (by key, or {"kind": "triggered_by"}), in, empty |
| list | contains, empty |
Semantics
- null compares false under every operator except empty. empty is true for null, "" and [].
- Decimals compare as exact decimal strings (no floats): "50000", 50000 and "50000.00" are equal; exponents are refused.
- A date field compares by calendar date in the version timezone; before/after are strict. A plain YYYY-MM-DD literal is a date in that timezone. A stored date at midnight — a DATE column, Laravel’s date cast, a date trigger input — keeps the calendar day it was stored as, whatever the version and app timezones. A date field that holds a time of day is the day that instant falls on in the version timezone; a datetime compares as an instant.
- A party field equals a model, its key, or the triggering user.
- An unregistered attribute is never read, even if it exists on the model.
Publish validation
| Problem | Code |
|---|---|
| Unknown field | unknown_field |
| Operator not allowed for the field type | operator_not_allowed |
| Literal of the wrong type | invalid_value |
| Undeclared input.* | unknown_input |
| steps.<id> that is not an ancestor | invalid_reference |
| Deeper than conditions.max_depth (6) or more than conditions.max_clauses (100) clauses | condition_too_complex |
Where conditions appear
condition.when, branch.cases[].when, wait.condition, wait.where, schedule for_each.where, and a trigger’s condition — a filter evaluated before an instance exists; false → no instance, listed as skipped in EmitResult.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.