Skip to content

Conditions ​

A condition checks one value and answers true or false. The If, ElseIf and Conditional Loop steps run their branch when it's true; the array steps find, filter, some, every and count check it on each element; and an app's table and progress color rules use it to pick a color.

Every condition has the same parts:

yaml
- group: flow
  name: ifConditional
  params:
    firstOperand: "{{ order.total }}"   # the value to check
    compareAs: number                   # how to read the values
    operator: gt                        # the comparison
    secondOperand: 1000                 # what to compare with
    actions:
      - group: system
        name: message
        params:
          text: Big order
FieldWhat it is
firstOperandThe value to check. Usually a variable: {{ status }} or {= items =}.
compareAsHow both values are read: string, number, date, boolean, array or object. Required.
operatorThe comparison. Which ones you can use depends on compareAs (see below).
secondOperandWhat to compare with. Not used by operators like empty or isTrue.
ignoreCaseOptional, true to treat upper and lower case as the same in text comparisons.

In the builder you pick Compare as first; the Operator list then shows only that type's operators, and Compare with disappears for operators that don't use it.

Try it ​

A self-checking workflow in the repository tries a few cases of each type. Each check prints ✅ or ❌ and the last step says whether all of them passed:

  • examples/actions/flow/conditions.yml — text, numbers, dates, booleans, arrays and objects, plus the array steps and a Conditional Loop. At the end, two commented-out steps show the messages you get for values that can't be compared.

Why the type matters ​

A value is converted to the chosen type before it's compared, and both sides are converted the same way. This is what makes text from templates compare correctly:

  • {{ }} always produces text. {{ count }} gives "10", not 10, and compared as text "10" is smaller than "9" (it compares letter by letter). With compareAs: number, both become numbers and 10 > 9 is true.
  • {= =} passes the value itself — a list stays a list, an object stays an object. Use it for array and object conditions: {{ fruits }} would turn the list into text (applebananacherry).

If a value can't be read as the chosen type, the step fails with a message saying which value and why, instead of quietly going to the else branch:

The value is "Banana", not a number (compared as number).
The value is "applebananacherry", not an array (compared as array).
Use {= name =} instead of {{ name }} to pass the value itself, not its text.

To check first whether a value can be read as a type, use isValid (below).

Operators by type ​

Text — compareAs: string ​

Numbers and true/false are read as their text (42 → "42"). Lists and objects aren't text and fail.

OperatorTrue when the value…Example secondOperand
eqequals the textactive
neqdoesn't equal the textactive
containscontains the text@acme.com
notContainsdoesn't contain the texttest
startsWithstarts with the textINV-
notStartsWithdoesn't start with the texttmp_
endsWithends with the text.pdf
notEndsWithdoesn't end with the text.tmp
matchesRegexmatches the regular expression^INV-\d{4}$
notMatchesRegexdoesn't match the regular expression^\s*$
lengthEqhas exactly this many characters6
lengthNeqdoesn't have this many characters6
lengthGt / lengthGtehas more than / at least this many characters3
lengthLt / lengthLtehas fewer than / at most this many characters255

Comparisons are case-sensitive: Banana doesn't equal banana. Add ignoreCase: true to make them equal — it applies to eq, neq, the contains, startsWith and endsWith families, and the regex operators.

yaml
firstOperand: "{{ status }}"
compareAs: string
operator: eq
secondOperand: inactive
ignoreCase: true          # "Inactive" and "INACTIVE" match too

Number — compareAs: number ​

Text that is a number is read as one ("1250", " 42 ", "42.0"). Empty text and anything else ("n/a") fail.

OperatorTrue when the value is…
eqequal to the number ("42" equals 42.0)
neqnot equal to the number
gtgreater than the number
gtegreater than or equal to the number
ltless than the number
lteless than or equal to the number
yaml
firstOperand: "{{ order.total }}"
compareAs: number
operator: gte
secondOperand: 1000

Date & time — compareAs: date ​

Reads ISO text like 2026-10-01 or 2026-10-01T14:30:00Z, dates, and timestamps in milliseconds. A date without a time is midnight in your time zone, the same as a date you type.

OperatorTrue when the value is…
eqthe same moment
neqa different moment
afterlater than the date
afterOrEqualthe same or later
beforeearlier than the date
beforeOrEqualthe same or earlier
yaml
firstOperand: "{{ invoice.dueDate }}"
compareAs: date
operator: before
secondOperand: "2026-12-31"

Boolean — compareAs: boolean ​

Reads true / false, and also "true", "yes", "on", "1", 1 as true and "false", "no", "off", "0", 0 as false (any case). Anything else fails.

OperatorTrue when the value…Needs secondOperand
isTrueis trueno
isFalseis falseno
eqequals true / falseyes
neqdoesn't equal true / falseyes
yaml
firstOperand: "{{ decision.approved }}"
compareAs: boolean
operator: isTrue

Array — compareAs: array ​

The value must be a list — pass it with {= list =}.

OperatorTrue when the list…Example secondOperand
containshas the itemadmin
notContainsdoesn't have the itemadmin
lengthEqhas exactly this many items3
lengthNeqdoesn't have this many items0
lengthGt / lengthGtehas more than / at least this many items10
lengthLt / lengthLtehas fewer than / at most this many items100

How contains matches an item:

  • Objects match by content: [{ id: 3 }] contains { id: 3 }.
  • A number matches its text, so [1, 2, 3] contains "2" typed in the builder.
  • ignoreCase: true makes text items match in any case (["Admin"] contains admin).
yaml
firstOperand: "{= user.roles =}"
compareAs: array
operator: contains
secondOperand: admin

Object — compareAs: object ​

The value must be an object (not a list) — pass it with {= object =}.

OperatorTrue when the object…Example secondOperand
hasKeyhas the propertyemail
notHasKeydoesn't have the propertydeletedAt
yaml
firstOperand: "{= customer =}"
compareAs: object
operator: hasKey
secondOperand: email

Every type ​

These work with any compareAs and take no secondOperand. Only isValid depends on the type; the others look at the value as it is.

OperatorTrue when the value…
existsis set (not null or missing)
notExistsis null or missing
emptyis null, missing, empty text "", an empty list [] or an empty object {}
notEmptyisn't empty. Numbers, true/false and dates are never empty.
isValidcan be read as compareAs — e.g. "1250" is a valid number, "Banana" isn't; "2026-10-01" is a valid date
yaml
# Only parse the date if the form field really holds one
firstOperand: "{{ form.birthday }}"
compareAs: date
operator: isValid

Checking for a missing property

To check whether an object has a property, use compareAs: object with hasKey / notHasKey on the object. A reference to a missing property, like {= customer.phone =}, gives back its own text instead of nothing, so notExists can't detect it.

Conditional Loop ​

A Conditional Loop runs its steps while the condition is true, checking it again before each pass with the variables' current values:

yaml
- group: flow
  name: conditionalLoop
  params:
    firstOperand: "{{ counter }}"
    compareAs: number
    operator: lt
    secondOperand: 5
    actions:
      - group: math
        name: add
        params:
          augend: "{= counter =}"
          addend: 1
        destination: counter

It stops after 10,000 passes as a safety guard.

Array steps ​

find, filter, some, every and count check a condition on each element of a list. They use the same compareAs, operator and ignoreCase, with key for the property to read and value for what to compare with:

yaml
- group: array
  name: filter
  params:
    array: "{= invoices =}"
    key: amount              # each element's amount (dots reach nested properties)
    compareAs: number
    operator: gt
    value: 100
  destination: bigInvoices

Leave out key to compare each element itself. One difference from If: an element whose value can't be read as the type (an amount of "n/a", a missing date) simply doesn't match — the step doesn't fail. A value that can't be read (comparing as a number with "lots") still fails the step, since that's a mistake in the workflow.

App color rules ​

A table column's highlight and a progress bar's or KPI card's colorRules are conditions too, plus a badge color. The first rule that matches colors the cell:

yaml
- key: amount
  format: currency
  highlight:
    - { compareAs: number, operator: lt, value: 0, color: red }
    - { compareAs: number, operator: gte, value: 5000, color: green }
- key: status
  highlight:
    - { compareAs: string, operator: eq, value: overdue, ignoreCase: true, color: red }

A cell that can't be read as compareAs (n/a in a number column) just doesn't match. See Table columns.

Not a condition: Switch ​

Switch / Case don't use conditions: each Case runs when the Switch value is exactly equal to its match. Use If / ElseIf for anything else.

Released under the Apache License 2.0.