Skip to main content
Every pattern below was run through the engine. The facts, the rules and the output are the real ones — copy them, change the names, and they will behave the same way.

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. Facts
Rules
Output
between 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
Rules
Output
Testing for the @ 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, use generateArray. Each input is [test, valueWhenTrue]; the tests that fail contribute nothing. Facts
Rules
Output
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. Facts
Rules
Output
You cannot inline the ladders into the +. A {condition, outcome} object placed inside an operator’s input is not resolved — it survives as a raw object, + parses it as NaN and discards it, and the total comes out as 0 with no complaint. Each branch must be its own rule.

Lookup 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.
With 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:
Result: 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
Rules
Output
Use 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
Rules
Output
Arithmetic is never element-wise. Multiplying unitPrices.value by quantities.value does not give you line totals — * flattens both arrays and multiplies everything into a single scalar, 1200 in this case. There is no operator that pairs two arrays. Use the wildcard pattern below, or compute the totals before the flow reaches the Rules step.
Note also that 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.
With 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 form parent/index/child and use a wildcard in the rule key. The engine expands it into one rule per entry in the parent list. Facts
Rules
Output
The parent list may be a comma-separated string as above, or an array. See wildcard fact keys for nesting.

Building a message

stringTemplate fills numbered placeholders in order. Inputs may be nested operators, so formatting happens inline. Facts
Rules
Output
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
Rules
Output, run on 27 August 2026:
dateDiff returns end - start, so the order of the first two inputs decides the sign.

Guarding a calculation

Division by zero returns NaN, which drops the fact — so a rule that looks harmless can leave a hole in the output. Guard it with a ladder. Facts
Rules
Output
The same applies to *: 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”.
With base.value of 420: 630, then 500, then 500.

Defaults

A rule key ending .defaultValue automatically supplies the matching .value when nothing else does.
Output
A fact wins over the default, and so does an explicit .value rule.
The fallback is disabled by the presence of a .value rule, not by whether that rule produces anything. A ladder with no default cell that matches nothing leaves you with no .value at all. Reference the default explicitly if you want both:
With severity.value of "low" this returns "normal".

Extracting from text

regex returns the first capture group. A pattern with no capture group returns nothing.
With email.value of "sam@example.com": "example.com" and true.
The pattern is written "[@](.+)$" rather than "@(.+)$" on purpose. A string beginning with @ is parsed as a dynamic value before regex sees it, and the step fails with unknown operator "(.+)$".
For initials and other joins with no separator, use stringTemplate rather than concat, which always inserts a space:
With "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.