Skip to main content

The payload

A Rules step takes one JSON object with three keys:
facts is the input data. rules says how to derive new values from it. context carries settings for the run — currently only timezone. Only rules is required.

Facts

Facts are a flat object. Keys are plain strings; the engine never treats a dot in a key as a path, so "orderTotal.value" is one key, not a nested object.
In a flow you normally map facts in from earlier steps with variable mapping:
Those $. expressions are resolved before the rules run. Rules read facts by name with the @fact: prefix. A rule’s own output becomes a fact for the rules that depend on it, so @fact: is also how one rule reads another.

The five rule shapes

Every entry in rules is keyed by the fact it produces. Its value is one of five things, and the engine decides which by inspecting the value’s structure.

1. Dynamic value

An object with operator and input. The most common shape.
Inputs nest without limit — an input element may itself be a dynamic value:
Result: 30. If input is not an array, the engine wraps it in one before the operator runs. "input": "@fact:quantities.value" is therefore how you feed an array fact to +, min or max as a whole.

2. Dynamic value shorthand

A string beginning with @ is parsed as "@operator:input".
Almost every use is @fact:. Any registered operator works — "@today:" returns today’s date — but for anything but a fact read the object form is clearer.
Any string starting with @ is treated as an operator call, wherever it appears — including inside an operator’s input. "@someone" is read as the operator someone, which does not exist, so the step fails with unknown operator "someone". This catches regular expressions in particular: "@(.+)$" never reaches regex. Rewrite it as "[@](.+)$", or build the literal with stringTemplate.

3. Rule cell

An object with an outcome, and optionally a condition and an outcomeMessage. When the condition resolves truthy, the outcome is produced. When it does not, the rule produces nothing at all — the key is absent from the output.
Both outcome and outcomeMessage may themselves be dynamic values. outcomeMessage is not visible to the rest of your flow. The engine returns it alongside the outcome, but the flow runner keeps only the outcome. Treat it as a note for someone reading the run, not as data.

4. Array of rule cells — the if/else ladder

Cells are tried in order and the first one whose condition resolves truthy wins. A final cell with no condition is the default.
With orderTotal.value of 650 this returns 0.1. Leave off the default cell and a value that matches nothing produces no fact at all rather than a fallback — silently. That is usually a bug, and it is one of the failures nothing warns you about, so write the default.
An array is always read as a ladder of rule cells. A plain array literal such as ["a", "b"] is not a list — the engine tries to read each element as a rule cell, finds no outcome, and drops the key, silently. To produce a list, use generateArray or concat-array.

5. Bare literal

Anything else becomes the outcome unchanged.
This is also why a stray key in the rules object turns into output. A _comment key is not ignored — it becomes a fact called _comment in the step’s result.

The .value and .defaultValue convention

Naming rules something.value is a convention, but .defaultValue is not — the engine special-cases it. Before evaluation, for every rule key ending .defaultValue, the engine looks for the matching .value key. If neither a fact nor a rule supplies one, it creates a rule "<name>.value": "@fact:<name>.defaultValue". So a default flows through automatically:
Output:
A fact wins over the default, and so does an explicit .value rule.
The fallback is wired up by the presence of a .value rule, not by whether it produces anything. If you write a .value ladder with no default cell and none of its conditions match, you get no .value at all — the .defaultValue does not step in. Reference it explicitly in the ladder’s final cell if you want both:

Evaluation order

You do not order rules yourself. The engine reads the @fact: references out of every rule, builds a dependency graph, and evaluates in an order that satisfies it. Rules with no relationship to each other are independent, and the order of keys in your JSON does not matter. A rule that references itself reads the incoming fact, not its own result, so {"n": {"operator": "+", "input": ["@fact:n", 1]}} with a fact n of 10 produces 11.
There is no cycle detection. If two rules reference each other the engine does not fail — it picks an order and returns whatever falls out. Given a.value = b.value + 10 and b.value = a.value + 20 with no facts, it returns a.value: 10 and b.value: 30. Nothing tells you the answer is meaningless.

When a rule produces nothing

This is the most important behaviour on this page. A rule can fail in two very different ways, and the difference decides how you find it.

What fails the step

Before a Rules step or a Condition step runs, the whole rules payload is checked against the engine’s operator list. A rule that names something the engine does not have never executes — the step fails, and the message names the rule and the operator:
The check walks nested operands too, so an unknown operator buried inside another operator’s input is found. Up to twenty problems are reported at once, separated by ;. A cell with no condition key at all is untouched by this — that is the default cell, and it is meant to have none.

What still disappears silently

Everything else. A rule that resolves to nothing is dropped from the output with no error, no warning and no log line, and the flow carries on with the fact missing. A rule that throws — jsonParse on text that is not JSON, say — is caught and produces the same silence. The engine also keeps an internal list of warnings, and nothing ever reads it. It is not returned, not logged, and not shown in the flow’s run history, so these causes give you nothing to search for. Two consequences worth planning around:
  • The remaining silent failures all look alike. A missing key does not tell you whether the condition was false, the maths produced NaN, or the outcome happened to equal its own fact. Narrow it by feeding the rule known facts in a test run and watching which keys appear.
  • Downstream steps see an absent key, not an error. A variable mapping pointing at it resolves to an empty array, and the flow continues with a hole in its data. Give rules that must always produce a value a default cell, or a .defaultValue sibling.

Reading the output in later steps

The step emits a flat object of {ruleName: outcome}. The wrapper the engine returns internally is stripped, so there is no .outcome to address:
A rule name containing a dot needs bracket notation downstream. Variable mapping is JSONPath, which reads . as a path separator. If your rules step is called rules_1:The whole .value convention depends on this. If you would rather not think about it, name your rules without dots — finalPrice reads as $.rules_1.finalPrice.

Wildcard fact keys

Fact keys of the form parent/index/child can be addressed with /*/ in place of the index. The engine expands the wildcard against the value of the parent key, which must be an array or a comma-separated string, and it looks at both parent and parent.value. Given these facts:
a wildcard reference collects every match into an array before the operator sees it:
Result: 3. A wildcard may also appear in a rule key, in which case the rule is expanded into one rule per parent value. This is the only way to do element-wise arithmetic — the arithmetic operators flatten arrays into a single scalar and never work element by element.
Output:
Note that the expanded keys — lines/A/total — are what appear in the output, and that a wildcard rule feeds a wildcard aggregate in the same step. Wildcards nest to five levels. The outermost level is /*/ and each level further in gains a star — /**/, then /***/, and so on. Each level’s parent list is the key up to that point:
Output:
If the parent key is missing the wildcard expands to nothing, which for + means 0 and for most other operators means the fact is dropped. Wildcards suit data that arrives already flattened. If you have an ordinary array of objects, jPath is simpler.

Time zones

context.timezone is a number — a UTC offset in hours. It is threaded into today, now, addDate and subtractDate.
Result: "2026-08-28T00:00:00.000+10:00". Named zones do not work. "Australia/Sydney", and the strings "UTC" and "LOCAL", are all ignored, and the server’s own offset is used instead. Only dateFormat accepts "UTC" and "LOCAL", and it takes them as its own third input rather than from context.

Things that look like they should work and do not

json-rules-engine syntax. The engine bundle contains a copy of json-rules-engine, but it is never called. Rules written with all, any, conditions or event keys do nothing — they are read as a bare literal and returned unchanged, or dropped. { "if": …, "then": …, "else": … }. There is no such shape. Use the array ladder. @fact: inside a literal outcome. References are only resolved in a dynamic value. An outcome that is a plain object or array is returned exactly as written:
produces the literal string "@fact:award.value" as the amount. Build objects with jsonParse over a stringTemplate, or emit the parts as separate facts. Rule cells nested inside an operator’s input. Only dynamic values are resolved inside an input. A {condition, outcome} object placed in an input survives as a raw object, and an arithmetic operator then parses it as NaN. Compute the branch as its own rule and reference it with @fact:. Short-circuiting. and and or receive inputs that have already been fully evaluated. Putting the cheapest test first changes nothing. Counting, filtering and set operations. The engine has no length, filter, reduce, union or intersection operator. See the operations reference for what to use instead.