Skip to main content
This is the complete operator registry. Every example on this page was run through the engine and the result shown beside it is the engine’s own output.
If an operator is not on this page, it does not exist. A rule naming one is rejected before the step runs, and the step fails with the rule and the operator named — unknown operator "arrayLength". That is the one rules mistake you are told about; several others are still silent. See when a rule produces nothing.

Rules that apply to every operator

Inputs are resolved first. By the time an operator runs, every @fact: reference and every nested {"operator": …, "input": …} inside its input has already been evaluated to a plain value. Nothing is lazy and nothing short-circuits. A non-array input is wrapped. "input": 4.2 reaches the operator as [4.2]. The one exception is not, which receives its input unwrapped. Three results drop the fact. undefined, NaN and Infinity all cause the key to be omitted from the output entirely. So does an outcome that is null, and so does an outcome equal to the fact of the same name that was already supplied as input. Aliases are exact synonyms. They resolve to the same function; there is no behavioural difference between >=, greaterThanInclusive and greaterThanOrEqual.

How numbers are parsed

Arithmetic and comparison operators put every input through the same parser before using it. It is narrower than JavaScript’s: NaN behaves differently per operator: + discards the offending value and carries on, while * propagates it and the whole fact is dropped.

Arithmetic

+

Sums its inputs. Nested arrays are flattened first. Values that do not parse as numbers are discarded rather than poisoning the sum, so a missing fact contributes nothing instead of breaking the rule.
Result: 17. With [10, "abc"] the result is 10, not an error. To sum a fact that is already an array, pass it as the whole input:
With quantities.value of [1, 2, 3] the result is 6.

-

Subtracts each subsequent input from the first. A single input is negated.
Result: 5. {"input": [10]} gives -10.

*

Multiplies its inputs. Needs at least two, otherwise it returns NaN and the fact is dropped. Nested arrays are flattened and everything is multiplied into a single scalar — it is not element-wise.
Result: 20. [[10, 20], [2, 3]] gives 1200, not [20, 60]. A null input makes the whole result NaN, so the fact disappears.

/

Divides the first input by the second. Division by zero returns NaN, dropping the fact.
Result: 2.5

%

Aliases: mod Remainder of the first input divided by the second.
Result: 1

power

Aliases: pow, ^ First input raised to the second.
Result: 1024

ceil

Rounds up to the nearest integer. Uses only the first input.
Result: 5

floor

Rounds down to the nearest integer.
Result: 4

round

Rounds to the nearest integer, halves going up.
Result: 5 There is no “round to N decimal places” operator. Use numberFormat if you want a formatted string, or multiply, round and divide if you want a number.

trunc

Discards the fractional part without rounding.
Result: -4

e

Euler’s number. Ignores its input, but input must still be present — use [].
Result: 2.718281828459045

log

Natural logarithm of the first input.
Result: 2.302585092994046

baseLog

Logarithm of the second input in the base given by the first. The order reads backwards from how it is usually written.
Result: 3 — this is log base 2 of 8.

min

Smallest of its inputs. Anything that does not parse as a number is dropped first; if nothing is left the result is NaN and the fact disappears.
Result: 3 min and max do not flatten. {"input": [[5, 3, 9]]} sees one array, parses it as NaN, and drops the fact. To take the minimum of an array fact, pass the fact as the whole input: {"operator": "min", "input": "@fact:xs"}.

max

Largest of its inputs, with the same rules as min.
Result: 9

Comparison

Each of these takes exactly two inputs: the value, then what to compare it against. The four ordering operators parse both sides as numbers; = and != use loose equality, so 5 and "5" are equal and true and 1 are equal.

=

Aliases: equal, equals
Result: true

!=

Aliases: <>, notEqual, notEquals
Result: true

>

Aliases: greaterThan
Result: true

>=

Aliases: greaterThanInclusive, greaterThanOrEqual
Result: true

<

Aliases: lessThan
Result: true

<=

Aliases: lessThanInclusive, lessThanOrEqual
Result: true

between

Takes [value, [low, high]] — the bounds are a nested array, not two extra inputs. An optional third input picks which ends are inclusive.
{"input": [40, 30, 50]} does not fail. The engine sees 30 where it expects a two-element array and returns false for every value you ever pass it. The bounds must be bracketed.
Result: true. The flat form [40, 30, 50] returns false.

notBetween

Same argument shape, negated. It also returns false when the bounds are not a two-element array.
Result: true

Membership

inArray

Aliases: in True when the first input appears in the array given as the second. Comparison is loose, so "5" matches 5. If the second input is not an array the result is false.
Result: true

notInArray

Aliases: notIn
Result: true

hasOptions

Aliases: options-in, optionsIn Set membership for strings only. The first input is a string, or a comma-separated string, or an array of strings; the second is the option or options to look for. A third input of "AND" requires all of them; the default is "OR", meaning any.
Result: true. With numbers — [[1, 2], ["1"]] — the result is false, because the fact is not an array of strings.

arrayContains

Aliases: contain, contains, arrayContain The reverse of inArray: the first input is the array, the second is what to look for. If the second input is itself an array, every element must be present unless a third input of "OR" is given. A first input that is not an array returns false.
Result: true. Without the "OR" it is false.

arrayNotContains

Aliases: notContain, notContains, arrayNotContain True when none of the sought values is present. With an array of values and the default "AND", every one of them must be absent.
Result: true

stringContains

Aliases: stringContain Substring test. The first input must be a string — a number returns false, it is not coerced. An array as the second input means all substrings must be present, or any of them with a third input of "OR".
Result: true

stringNotContains

Aliases: stringNotContain
Result: true. Note that a non-string first input returns false here too, so this is not the strict negation of stringContains.

Strings

substring

Takes [text, start] or [text, start, end]. The third input is an end index, not a length. Negative indices count from the end. A non-string first input gives "".
Result: "C". ["ABC123", 2] gives "C123", and ["ABC123", -3] gives "123".

concat

Joins every input with a single space. There is no separator argument.
Result: "Hello Sam"

join

Joins every input with a comma, discarding empty and null values first. There is no separator argument.
Result: "a,c" To join with something other than a comma, use jPath with its third input.

stringTemplate

First input is the template; the rest fill numbered placeholders {{1}}, {{2}} and so on, in order. A placeholder with no matching value is replaced with an empty string. Values are stringified, so an object becomes [object Object].
Result: "Hi Sam, you have 3 tasks" A placeholder may repeat: ["{{1}}-{{1}}", "x"] gives "x-x".

regex

Matches the second input, as a regular expression, against the first and returns the first capture group. A pattern with no capture group, or no match at all, returns nothing and the fact is dropped. Escape backslashes for JSON.
Result: "4471". ["order-4471", "order-\\d+"] — no capture group — drops the fact.
A pattern beginning with @ is read as a dynamic value before regex ever sees it, so "@(.+)$" fails the step with unknown operator "(.+)$". Write it as "[@](.+)$".

split

Splits a string into an array. Second input is the separator, defaulting to a comma. A non-string or empty first input drops the fact.
Result: ["a", "b", "c"]

JSON and JSONPath

jPath

Aliases: jsonPath Runs a JSONPath query over the first input, which may be an object, an array, or a JSON string. Returns an array of matches. An optional third input joins the matches into a string instead.
With lines.value of [{"price": 12.5}, {"price": 40}] the result is [12.5, 40]. Filter expressions work:
The third input is the only way to join with something other than a comma or a space:
With tags.value of ["billing", "urgent"] the result is "billing, urgent". $.length gives you a count, which the engine has no dedicated operator for:
Result: 2. The jPath alone returns [2]; the + reduces it to a scalar.

jsonParse

Parses the first input as JSON. Invalid JSON drops the fact.
Result: {"a": 1}

jsonStringify

Serialises the first input.
Result: "{\"a\":1}"

Arrays and lookups

map

Aliases: lookup A lookup table. Takes [key, table] with an optional [key, table, fallback]. The table may contain a "$default" key. Without either a match, a $default or a fallback, the fact is dropped.
With region.value of "fr" the result is 0. An array key walks the table one level per element, which gives you nested lookups:
Result: 0.2

sort

Sorts an array. Inputs are [array], [array, direction], [array, direction, path] or [array, direction, path, parseAsNumber]. Direction is "asc" unless it is "desc" or "descending". A non-array first input drops the fact.
Result: [{"n": 1}, {"n": 2}] Sorting is string-wise unless you pass true as the fourth input: [["10", "9"], "asc", "", true] gives ["9", "10"], where without it you would get ["10", "9"].

sortString

Sorts a delimited string and rejoins it. Inputs are [text, direction, delimiter, parseAsNumber]; the delimiter defaults to a comma. A non-string first input drops the fact.
Result: "c|b|a"

generateArray

Aliases: generate-array, array.generate Builds an array by walking its inputs. A plain input is appended as-is. An input that is itself a three-element array is read as [test, whenTrue, whenFalse], and whichever branch is chosen is appended — flattened one level, so a branch may contribute several elements. Undefined values are removed. If nothing survives, the fact is dropped.
Result: ["yes", "no", "always"] This is how you build a conditional list — a set of tags or reasons that depend on other facts — without a filter operator.

concat-array

Concatenates arrays. Non-array inputs are ignored rather than appended. Note the hyphen: concatArray is not a registered name.
Result: [1, 2, 3]

empty

True when the input is undefined, null, "", an empty array or an empty object. Given several inputs, all of them must be empty.
With openIssues.value of [] the result is true.

notEmpty

Aliases: not-empty The negation of empty.
Result: true
There is no length, count, filter or reduce operator. For a count, use jPath with $.length as shown above. For a conditional list, use generateArray. For per-element arithmetic, use wildcard fact keys — arithmetic operators flatten arrays into a single scalar and are never element-wise.

Dates

Every date operator returns an ISO 8601 string with an offset, except timeNow which returns a number and dateDiff which returns a number. Durations are written as a number followed by a single case-sensitive letter: "1M" is one month and "1m" is one minute. All three date operators accept spelled-out units as well as the letters: "days", "months", "minutes" and the rest, singular or plural, in any case. dateDiff also takes "ms" for milliseconds.
A unit none of them recognises — "2fortnights", or "2 days" with a space — is skipped silently. addDate and subtractDate return the date unchanged and dateDiff drops the fact, in both cases with no error. Check the spelling.

today

Start of the current day. Ignores its input.
Result: "2026-08-27T00:00:00.000+01:00"

now

The current instant.
Result: "2026-08-27T17:18:50.239+01:00"

timeNow

The current instant as milliseconds since the epoch.
Result: 1787847530246

addDate

First input is a date; any input containing a duration is applied to it. Several durations may be given and they are applied in order.
Result: "2027-03-04T00:00:00.000Z" Month arithmetic clamps: ["2026-01-31", "1M"] gives "2026-02-28T00:00:00.000Z".

subtractDate

The same, subtracting.
Result: "2026-02-01T00:00:00.000Z"

dateDiff

Inputs are [start, end] or [start, end, unit], with the unit defaulting to "d". Returns end - start, so an end before the start is negative. The unit may be one of the seven letters, "ms", or a spelled-out name — "day", "days", "month", "months", "minute", "minutes" and so on. An unrecognised unit drops the fact, silently.
Result: 59. The same call with "months" returns 2, and with "ms" returns 5097600000.

dateFormat

Inputs are [value, pattern] or [value, pattern, timezone]. The pattern uses date-fns tokens. The third input is "ORIGINAL" (the default, keeping the offset the value carried), "UTC", "LOCAL", or a number — a UTC offset in hours.
Result: "27/08/2026"
Result: "2026-08-28 09:30"

toISO

Converts milliseconds since the epoch to a UTC ISO string. With no input it returns the current instant.
Result: "2026-08-17T20:53:20.000Z"

Logic

and, or and not coerce with JavaScript truthiness. 0, "", null and undefined are false; an empty array or empty object is true. Use empty rather than not when you mean “has no items”.

and

Aliases: &, && True when every input is truthy. It does not short-circuit — every input was already evaluated before and ran.
Result: true when both are.

or

Aliases: |, || True when any input is truthy.
Result: true

not

Aliases: ! The one operator whose input is not wrapped in an array. Given a scalar it returns a boolean; given an array it returns an array of booleans, which is almost never what a condition wants.
With emailValid.value of true the result is false. Written as "input": [true] it returns [false] — an array, which is truthy.

Composing

expression

Aliases: exp Evaluates an infix array: operand, operator name, operand, operator name, and so on. Evaluation is strictly left to right with no operator precedence. Nest an array to force a different order.
Result: 30 — (10 + 5) * 2, not 20.
Result: 30, this time explicitly. The infix position accepts a slightly narrower list than the registry. Everything in Arithmetic, Comparison, Membership, Strings, JSON and JSONPath, Dates, and, or, map and lookup may appear there. expression itself, not, generateArray, sort, sortString, split, empty, notEmpty, concat-array, numberFormat and their aliases may not — an unrecognised name drops the fact. A string is not an expression. ["10 + 5"] returns the string "10 + 5" unchanged, and an array of one or two elements always returns its first element.

Numbers

numberFormat

Formats a number for display. Inputs are [value], [value, digits] or [value, digits, locale]. digits fixes both the minimum and maximum fraction digits and must be a number. The locale defaults to en-AU. Returns a string; a value that is not numeric drops the fact.
Result: "1,234,567.89". With "de-DE" as the third input: "1.234.567,89".

Reading facts

fact

Reads a fact by name. This is the operator behind the "@fact:name" shorthand, and the two forms are identical.
With an array input it returns an array of the values, skipping any that are missing:
With a.value of 1 and b.value of 2 the result is [1, 2]. A fact that does not exist resolves to nothing. Inside + that is harmless — it is discarded and the sum continues — but inside * it drops the whole fact.
fact is the only operator the engine special-cases, and the only one whose name may appear in the "@name:input" shorthand and be worth using. Any registered operator works in that shorthand — "@today:" returns today’s date — but for anything other than reading a fact the object form is clearer.

Names that are not operators

Every name below appears in older versions of this documentation or reads like a plausible operator. None of them exists, and each one fails the step it appears in rather than producing a wrong answer.