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.
$. 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 inrules 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 withoperator and input. The most common shape.
input element may itself be a dynamic value:
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".
@fact:. Any registered operator works — "@today:" returns
today’s date — but for anything but a fact read the object form is clearer.
3. Rule cell
An object with anoutcome, 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.
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 nocondition is the default.
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.
5. Bare literal
Anything else becomes the outcome unchanged._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:
.value rule.
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.
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
.defaultValuesibling.
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:
Wildcard fact keys
Fact keys of the formparent/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:
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.
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:
+ 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.
"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 ofjson-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:
"@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.