Banded rates
The commonest shape: pick a rate from a band, apply it, report the result. The ladder is tried top to bottom and the first matching condition wins, so put the bands in order and finish with a cell that has no condition. Factsbetween takes its bounds as a nested array. Written flat as
[value, 500, 999.99] it returns false for everything, silently — which in a
ladder means the band is never reached. Nothing catches this: the operator name
is real, so only the answer is wrong.
Validation with one message
Test each requirement as its own rule, combine them, and use a second ladder to pick the message. Ordering the message ladder puts the requirements in the order you want them reported. Facts@ takes a regex rather than the obvious
{"operator": "stringContains", "input": ["@fact:email.value", "@"]}. A bare
"@" is a dynamic value with an empty operator name, so that rule fails the step
outright — see dynamic value
shorthand. Bracketing
it as "[@]" is the way past that anywhere an @ has to be a literal.
Note how not is given its input unwrapped — "input": "@fact:nameProvided.value"
rather than ["@fact:nameProvided.value"]. not is the one operator that does
not wrap a scalar input, and given an array it returns an array of booleans,
which is always truthy.
Every reason, not just the first
When you want the full list rather than the first match, usegenerateArray.
Each input is [test, valueWhenTrue]; the tests that fail contribute nothing.
Facts
When no test passes,
generateArray produces nothing and declineReasons.value
is absent from the output rather than an empty array. notEmpty still
returns false, so declined.value is correct — but a downstream step reading
the list must cope with the key not being there.Scoring
Points from independent rules, summed, then banded. Keeping each contribution as its own rule makes the total explainable when someone asks why a record scored what it did. FactsLookup tables
map turns a table into a rule. $default catches everything unlisted; without
it, and without a third input, a miss drops the fact entirely.
region.value of "fr" the result is 0.
An array key walks the table one level per element, which gives you a
two-dimensional lookup:
0.2.
Multi-condition eligibility
Name each test, then combine. This costs nothing at runtime —and receives
inputs that were all evaluated anyway — and it means the step’s output shows you
which test failed.
Facts
empty rather than not for “has no items”: an empty array is truthy, so
not on one returns false.
Working with an array of objects
jPath pulls values out of an array fact. It returns an array, which arithmetic
operators accept directly.
Facts
min and max do not flatten. "input": "@fact:unitPrices.value"
works; "input": ["@fact:unitPrices.value"] sees one array, cannot parse it as a
number, and drops the fact.
Counting
There is no length operator.jPath with $.length returns a one-element array,
and + reduces it to a number.
tags.value of ["billing", "urgent", "vip"]: 3 and true.
Per-element arithmetic with wildcard keys
When you genuinely need a calculation per item, flatten the data into keys of the formparent/index/child and use a wildcard in the rule key. The engine
expands it into one rule per entry in the parent list.
Facts
Building a message
stringTemplate fills numbered placeholders in order. Inputs may be nested
operators, so formatting happens inline.
Facts
concat joins with a single space and join with a comma, neither of which is
configurable — stringTemplate is the only operator that gives you control over
the text between the parts.
Dates
Durations are a number and a single case-sensitive letter:y, M, w, d,
h, m, s. 1M is a month and 1m is a minute. Spelled-out units are
ignored.
Facts
dateDiff returns end - start, so the order of the first two inputs decides
the sign.
Guarding a calculation
Division by zero returnsNaN, which drops the fact — so a rule that looks
harmless can leave a hole in the output. Guard it with a ladder.
Facts
*: a null or missing input makes the whole result NaN.
+ is the exception — it discards values it cannot parse and carries on.
Clamping
min caps a value and max floors it. Read them as “no more than” and “no less
than”.
base.value of 420: 630, then 500, then 500.
Defaults
A rule key ending.defaultValue automatically supplies the matching .value
when nothing else does.
.value rule.
Extracting from text
regex returns the first capture group. A pattern with no capture group
returns nothing.
email.value of "sam@example.com": "example.com" and true.
For initials and other joins with no separator, use stringTemplate rather than
concat, which always inserts a space:
"Sam" and "Okonkwo": "SO". substring’s third input is an end
index, not a length.
Habits worth keeping
- Name every intermediate step. The engine works out the order, so splitting a calculation across several rules costs nothing and makes the output self-explaining when something is wrong.
- Always write the default cell. A ladder that matches nothing produces no fact, and a missing fact is indistinguishable from a broken one.
- Check the operator name against the reference before debugging anything else. An operator that does not exist produces no error.
- Avoid dots in rule names unless you are ready to write
$.rules_1['name.value']downstream. - Do not reorder rules for speed. Nothing short-circuits; every input is evaluated before the operator that uses it runs.