Skip to content
Talk to our solutions team

Rule language reference

Everything you may write inside a when or a then statement comes from one small expression grammar. This page is that grammar in full: what parses, what it evaluates to, and what the language has no syntax for at all.

The rule header, the execution cycle and rule-set loading are on Writing rules, the callable surface, in, out, audit and the helper namespaces, is on Built-in functions.

rule Name ["description"] [salience N] { when expression then statement; ... }
statement = target = expression // also += -= *= /=
| expression // in practice, a method call
expression = expression * / % expression
| expression + - & | expression
| expression == != > < >= <= expression
| expression && expression
| expression || expression
| [!] ( expression )
| atom
atom = literal
| name
| atom . name // member access
| atom [ expression ] // index
| name ( args ) // function call
| atom . name ( args ) // method call
| ! atom

That is the complete list of forms. There is no other statement kind, no other operator and no other way to construct a value.

PositionConstraint
whenMust evaluate to a boolean. when 1 fails with the when is not a boolean expression : 1
Right side of an assignmentAny expression
A call argumentAny expression, including a nested call
Inside [ ]Any expression: rows[1 - 1] works. One level of indexing only
salienceNot an expression. Integer literal only

Tightest first. All binary operators are left-associative.

LevelOperatorsNotes
1. [ ] ( )Member access, index, call. Postfix, applied left to right
2!Applies to a single atom or a parenthesised expression
3* / %
4+ - & |Bitwise & and | sit here, tighter than comparison
5== != > < >= <=
6&&Short-circuits
7||Short-circuits

1 + 2 * 3 == 7 is true. false && false || true is true. 6 & 3 == 2 is true, the & binds first, unlike C where the comparison would win. Parenthesise bitwise operands if you use them at all.

! binds to one atom, not to a comparison: !true == false parses as (!true) == false. To negate a comparison, parenthesise it, !(1 > 2).

There is no ternary, no in operator, no between, no + that produces anything but a number or a string, and no unary minus outside a numeric literal, the - in -5 belongs to the literal, so -x where x is a variable fails the load; write 0 - x.

&& and || short-circuit, and that is the only way to make a conditional access safe:

when
out.Has("tier") && out.Get("tier") == "gold"

The guard is required, not defensive. Without it the right-hand comparison runs against a value that is not there and aborts the whole execution, see Comparison below.

A guard that reads out is only half the job: the rule that wrote the value must also call Changed("out"), or a later rule’s when keeps the reading it memoised on the first cycle and never fires. Rule patterns has the full idiom.

FormExampleEvaluates to
Double-quoted string"invoice"string
Single-quoted string'invoice'string
Decimal integer42, -5int64
Hexadecimal integer0x1F (31), -0x10 (−16)int64
Octal integer010 (8)int64
Decimal float1.5, .5, 1.5e2, -1.5float64
Hexadecimal float0x1p-2 (0.25)float64
Booleantrue, falsebool
Nilniluntyped nil

Strings take backslash escapes: "say \"hi\"" and "a\nb" both work, the doubled-quote form "a""b" lexes as one token but fails the load when it is unquoted.

true, false and nil are matched case-insensitively, like the keywords.

Arithmetic follows the literal types, and the results surprise people coming from Go.

ExpressionResultType
2 + 35int64
3 * 412int64
7 / 23.5float64
1 / 30.3333333333333333float64
1 / 0+Inffloat64
1 + 2.53.5float64
7 % 21int64
9223372036854775807 + 1-9223372036854775808int64
"n=" + 5"n=5"string
"n=" + 2.5"n=2.500000"string

Four things follow from that table:

  • Division always yields a float. 7 / 2 is 3.5, never 3. There is no integer division operator and no truncation. Anything you divide is float64 from that point on, including as a function argument.
  • Division by zero does not fail. It produces +Inf and the rule carries on, so a zero denominator surfaces later as a nonsense output rather than an error.
  • % is int64-only. 7 % 2.0 aborts the execution with can not use data type of float64 in modulo; "a" % 2 aborts with the string form of the same message.
  • Integer overflow wraps silently at the int64 boundary.

Float equality is exact-bit equality: 0.1 + 0.2 == 0.3 is false. Compare with a tolerance instead, math.Abs(0.1 + 0.2 - 0.3) < 0.000001 is true.

+ concatenates when either side is a string, formatting a number operand as it goes, a float concatenates in fixed-point form, "n=" + 2.5 is "n=2.500000", not "n=2.5". Use strings.Sprintf rather than + when the exact text matters.

OperandsBehaviour
Two numbersCompared numerically across int64 and float64. 1 == 1.0 is true
Two stringsLexicographic. "abc" < "abd" is true
Two booleans== and != only
String on the left, number on the right, with == / !=No error. "5" == 5 is simply false
Number on the left, string on the right, with == / !=Aborts: can not use data type of string in EQ comparison
String vs number, with > < >= <=Aborts either way: "5" > 5 gives can not compare data type of string to int64 in GT comparison, 5 > "5" gives can not use data type of string in GT comparison

Mixed-type equality is not symmetric: which side the string is on decides whether you get false or a dead execution. Never rely on == to compare across types, convert first.

Reading a key that is not in a map fact fails the same way, with this node identified as "<fact>" have no selector with specified key.

&&, || and ! take booleans and nothing else. There is no truthiness:

when 1 && true // can not use data type of int64 in Logical AND comparison
when !1 // the when is not a boolean expression : !1

when accepts exactly one expression. Two conditions on consecutive lines is a load error, not an implicit AND, join them with &&.

Which accessor is legal depends on the shape the fact decoded to, not on how the value looks.

Fact typeAccessorExample
Struct or pointer to structDot, exported fields only. No surface supplies oneorder.Total > 1000
Object fact. A -f fact file, or a nested object on the POST /rule bodyBracketorder["Total"] > 1000
Slice or arrayBracket, integer indexrows[0]
Result of a callBracket, chained directly"a,b".Split(",")[1]
A value reached through one bracket alreadyNeither. Both accessors abortdoc["header"]["total"]

Both forms also work on the left of an assignment, and both mutate the caller’s fact in place:

then
order.Approved = true;
page["reviewed"] = "yes";

The index may be any expression, including a variable: k = "Total"; order[k].

Strings are not indexable. "abc"[0] panics with a nil pointer dereference.

.Len(), .ToUpper(), .In(...) and the rest are methods on a value, not grammar, see Methods on values.

name = expression; in a then block creates or overwrites a top-level name. +=, -=, *= and /= also parse; /= follows the division rule, so x = 7; x /= 2 leaves x at 3.5. Assignment is a then-only construct, when x = 1 fails the load. Write when x == 1.

Variables hold scalars, strings, numbers, booleans, and survive across rules within one execution.

A name starts with a letter (including a Unicode letter) and continues with letters, digits or _. Step_1_Do is valid; 1Step fails the load.

Names are case-sensitive everywhere, including the string you pass to Retract. A name written in a when that no fact binds aborts the entire execution with got non existent key <name>.

Each of these fails at load with a parse error. None is a runtime limitation you can work around with a flag.

You might writeWrite instead
if c { ... } else { ... }Two rules. The second carries the negated condition at a lower salience
for i = 0; i < 3; ... { }One rule that re-fires, driven by a counter fact; see Rule patterns
c ? a : bTwo rules
"a" in "a","b""a".In("a","b"). A method on a string value, not an operator
["a", "b"]Nothing. Supply the array as a top-level fact. A key in a -f file, or a top-level key on the POST /rule body
{"a": 1}in.NewMapWithValues("a", 1), usable only as a direct call argument
Finding{RuleID: "x"}Nothing: but nothing needs it. findings takes strings; see below
-x on a variable0 - x
when x = 1when x == 1
salience 1.5An integer literal. salience -5 is legal, salience 1.5 is not
A then statement without a trailing ;Terminate every then statement with ;
An empty thenAt least one statement

There is also no return, break or continue; no variable declaration or type annotation; no function, import or include; and no string interpolation. A .grl file contains rule declarations and comments, nothing else.

Branching is expressed as a second rule with the opposite condition at a lower salience, Rule patterns has the idiom.