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.
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:
quantities.value of [1, 2, 3] the result is 6.
-
Subtracts each subsequent input from the first. A single input is negated.
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.
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.
2.5
%
Aliases: mod
Remainder of the first input divided by the second.
1
power
Aliases: pow, ^
First input raised to the second.
1024
ceil
Rounds up to the nearest integer. Uses only the first input.
5
floor
Rounds down to the nearest integer.
4
round
Rounds to the nearest integer, halves going up.
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.
-4
e
Euler’s number. Ignores its input, but input must still be present — use [].
2.718281828459045
log
Natural logarithm of the first input.
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.
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.
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.
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
true
!=
Aliases: <>, notEqual, notEquals
true
>
Aliases: greaterThan
true
>=
Aliases: greaterThanInclusive, greaterThanOrEqual
true
<
Aliases: lessThan
true
<=
Aliases: lessThanInclusive, lessThanOrEqual
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.
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.
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.
true
notInArray
Aliases: notIn
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.
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.
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.
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".
true
stringNotContains
Aliases: stringNotContain
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 "".
"C". ["ABC123", 2] gives "C123", and ["ABC123", -3] gives "123".
concat
Joins every input with a single space. There is no separator argument.
"Hello Sam"
join
Joins every input with a comma, discarding empty and null values first. There
is no separator argument.
"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].
"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.
"4471". ["order-4471", "order-\\d+"] — no capture group — drops the
fact.
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.
["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.
lines.value of [{"price": 12.5}, {"price": 40}] the result is
[12.5, 40].
Filter expressions work:
tags.value of ["billing", "urgent"] the result is "billing, urgent".
$.length gives you a count, which the engine has no dedicated operator for:
2. The jPath alone returns [2]; the + reduces it to a scalar.
jsonParse
Parses the first input as JSON. Invalid JSON drops the fact.
{"a": 1}
jsonStringify
Serialises the first input.
"{\"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.
region.value of "fr" the result is 0.
An array key walks the table one level per element, which gives you nested
lookups:
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.
[{"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.
"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.
["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.
[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.
openIssues.value of [] the result is true.
notEmpty
Aliases: not-empty
The negation of empty.
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, excepttimeNow
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.
today
Start of the current day. Ignores its input.
"2026-08-27T00:00:00.000+01:00"
now
The current instant.
"2026-08-27T17:18:50.239+01:00"
timeNow
The current instant as milliseconds since the epoch.
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.
"2027-03-04T00:00:00.000Z"
Month arithmetic clamps: ["2026-01-31", "1M"] gives "2026-02-28T00:00:00.000Z".
subtractDate
The same, subtracting.
"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.
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.
"27/08/2026"
"2026-08-28 09:30"
toISO
Converts milliseconds since the epoch to a UTC ISO string. With no input it
returns the current instant.
"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.
true when both are.
or
Aliases: |, ||
True when any input is truthy.
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.
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.
30 — (10 + 5) * 2, not 20.
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.
"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.
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.