Expressions
RuleFlow’s expression language is the small, total, side-effect-free
language used inside decision_table cells, expression nodes, switch
cases and output bindings. It is a bespoke safe subset, not an embedded
general-purpose interpreter (not Rhai, not Lua, not JS) — deliberately, for
two reasons:
- Determinism / auditability. A decision must replay to the identical result forever, and produce the same answer in the browser (WASM) and on the server. That rules out clocks, randomness and IO.
- Stability. Once a rule references an operator or function, changing or removing it can silently alter a customer’s logic. The surface is small, explicit and versioned alongside the model.
Guarantees
- Total & pure. No IO, clock or randomness. No loops, no recursion, no user-defined functions — the only callable surface is the fixed function library below. Evaluation always terminates.
- Decimal arithmetic. Numbers are exact decimals, never
f64— financial precision by construction, not by convention. - Cross-target parity. The same evaluator compiles to native and to WebAssembly, so a browser preview and a server run evaluate identically.
Types
null, bool, number (decimal), string, date, array, object.
Literals: numbers (42, 0.35), double-quoted strings with escapes
(\" \\ \n \t), true, false, null, array literals ([a, b, c]). There
is no date or object literal — both enter only via input data.
Grammar
Lowest to highest precedence:
or → and ( "or" and )*
and → eq ( "and" eq )*
eq → cmp ( ( "=" | "!=" ) cmp )*
cmp → add ( ( "<" | "<=" | ">" | ">=" | "in" ) add )*
add → mul ( ( "+" | "-" ) mul )*
mul → unary( ( "*" | "/" | "%" ) unary )*
unary → ( "-" | "not" ) unary | postfix
postfix → primary ( "." IDENT | "[" or "]" )*
primary → NUMBER | STRING | "true" | "false" | "null"
| "[" args "]" | IDENT | IDENT "(" args ")" | "(" or ")"
a.b reads an object field; a[i] indexes an array or string by an integer.
Operators
- Arithmetic
+ - * / %on numbers.+also concatenates two strings ("a" + "b"→"ab"); mixing a number and a string is an error. - Equality
=/!=is type-aware and numeric-tolerant:1 = 1.0istrue. (There is no==—=alone is equality.) - Ordering
< <= > >=compares numbers, dates and strings (lexicographically); comparing incomparable types is an error, not a silentfalse. - Membership
x in [a, b, c]istruewhenxequals any element. - Logical
and/orshort-circuit;notnegates a boolean. A non-boolean operand to a logical operator is an error.
Functions
| Function | Arity | Notes |
|---|---|---|
if(cond, a, b) | 3 | lazy — only the taken branch is evaluated |
min(…), max(…) | ≥1 numeric | variadic |
abs(n) | 1 numeric | |
round(n), floor(n), ceil(n) | 1 numeric | |
len(x) | 1 | string (chars), array or object length |
contains(hay, needle) | 2 | substring (string) or element membership (array) |
startsWith(s, prefix) | 2 | strings |
lower(s), upper(s) | 1 | strings |
coalesce(…) | ≥0 | first non-null argument, else null |
dateDiffDays(a, b) | 2 dates | a - b in whole days |
isBefore(a, b), isAfter(a, b) | 2 dates |
Calling an unknown function, the wrong arity, or the wrong argument type is a typed error surfaced to the author — never a silent default.
Decision-table cell conditions
Table cells use a compact predicate mini-language, one per input column, tested against that column’s value:
| Form | Meaning |
|---|---|
- (or empty) | wildcard, always matches |
>= 18, < 100, != 0, = "GOLD" | comparison / (in)equality |
[18..65], (0..100], [1..10) | range; [/] inclusive, (/) exclusive |
in ["A", "B"] | set membership |
bare expression ("GOLD", 5, tier) | equality against the column value |
The right-hand side of a cell is a full expression, so it may reference
other inputs — e.g. <= creditLimit in one column reading a value computed
in another.
Where it’s used
expression nodes, switch case when predicates, output bindings
({ "expr": … }), and decision-table cells all share one parser and
evaluator — semantics are identical everywhere the language appears.
Versioning
The expression language version is coupled to the project’s model_version
— there is no separate version number. Additive changes (a new pure
function, a new operator) are a minor, backward-compatible bump. Removing a
function/operator, or changing evaluation semantics, is a breaking, major
bump. A stored release pins a model_version, and the engine refuses to run
a model whose version line it does not implement.