NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

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 typeAllowed operators
string, textequals, in, empty, contains (case-sensitive; add "case": "insensitive")
integer, decimalequals, in, gt, lt, between, empty
booleanequals, empty
date, datetimeequals, before, after, between, empty
enumequals, in, empty
partyequals (by key, or {"kind": "triggered_by"}), in, empty
listcontains, 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

ProblemCode
Unknown fieldunknown_field
Operator not allowed for the field typeoperator_not_allowed
Literal of the wrong typeinvalid_value
Undeclared input.*unknown_input
steps.<id> that is not an ancestorinvalid_reference
Deeper than conditions.max_depth (6) or more than conditions.max_clauses (100) clausescondition_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 crypto

By 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.