Rule language reference
Every operator, block and literal of the rule language in one place.
This page is the complete reference of the rule language - every operator, block and literal in one place. For a step-by-step introduction, start with the rule engine tutorial, and for guidance on writing fast rules, read about performance.
Operators
Each operator has an English spelling, and most have a symbol alias too. Both forms mean exactly the same thing and can be mixed freely, even within one rule.
| English form | Alias | Example |
|---|---|---|
is | == | title is 'Manager' |
is not | != | transaction_type is not customer_type |
is less than | < | amount is less than 100 |
is at most | <= | weight <= 20 |
is at least | >= | incident_severity is at least 5 |
is more than | > | amount is more than 1000 |
is between | - | amount is between 100 and 1000 |
is one of | in | category is one of 'fixed', 'variable' |
is not one of | not in | channel is not one of 'fax', 'pager' |
matches | =~ | doc_id matches 'INV-\d{4}' |
is true | - | premium_services_enrolled is true |
is false | - | account_suspended is false |
Notes on individual operators:
is betweentakes two values joined byand, and both boundaries are inclusive -amount is between 100 and 1000matches 100 and 1000 themselvesmatchescompares against a regular expression anchored at the start of the value -doc_id matches 'INV-'matchesINV-123but not2024-INV-123. The pattern must be a quoted literalis trueandis falseneed a real boolean on input, not merely a truthy or falsy value
The right side of a comparison can be a literal or a reference to another input field, including dotted paths:
List membership
is one of and is not one of accept one or more comma-separated values. Brackets around the list are optional and have no meaning, so all three of these are the same condition:
When the right side is a single reference rather than literals, it means membership in the collection that the reference resolves to. This works with defaults:
defaults
allowed_types = ['power_plant', 'water_treatment', 'government', 'hospital']
when
facility_type is one of default.allowed_types
.. and with collections coming directly from input data:
Blocks
A rule is made of blocks, each keyword alone on its own line with the content indented under it:
| Block | Required | Content |
|---|---|---|
rule | Yes | The rule's name, one line, letters, digits and underscores |
docs | No | Free-form documentation text, any number of lines |
defaults | No | name = value lines with concrete values |
when | Yes | Conditions, one per line |
then | Yes | target = value lines applied when the rule matches |
else | No | target = value lines returned when the rule does not match |
A complete rule using every block:
rule
TELCO_002
docs
Flags high-value transactions in categories
that need a manual review.
defaults
min_transaction_amount = 5000
when
transaction_type is not customer_type and
transaction_amount is more than default.min_transaction_amount and
transaction_category is one of 'fixed', 'variable'
then
score = 85
notification_channel = 'email', 'app_alert'
else
routing = 'manual'
Defaults are merged into the input for keys the input does not contain and are referenced from conditions as default.<name>. The else block applies to single-rule matching only - when a whole ruleset is matched, non-matching rules contribute nothing.
Conditions and logic
Conditions live in the when block, one per line, and every line except the last ends with and or or:
Parentheses are not part of the language - when logic needs grouping, split it into two rules. There is no not keyword either, negation is expressed with is not and is not one of.
Literals
Values in conditions, defaults, then and else blocks can be:
- Strings -
'text',"text"orr'raw text'. Regular expression patterns work unescaped, so'\d{4}'means four digits - Numbers - integers and floats
- Booleans -
trueandfalse - Datetimes -
d'2025-01-01T00:00:00' - Lists -
['gold', 'platinum']or bare comma lists,'sms', 'email' - Objects -
{'key1': 'value1'} - References to input fields, including dotted paths -
customer_type,order.amount
Comments
Comments start with # and run to the end of the line, anywhere outside a quoted string:
Missing fields are loud errors
When a condition or a then value refers to a field the input does not have, the rule never silently evaluates to false. Instead, matching raises RuleEvaluationError with a readable message:
The exception includes the rule's name in .rule_name and the field in .field. The one way to make a field optional is to give it a defaults entry - the default fills in before evaluation whenever the input lacks that key.
Comparing values of incompatible types, such as a string with is at least 700, raises the same error with a data type mismatch message. The exceptions are is and is not, which return a plain false and true respectively when types differ.
Rulesets and names
One .zrules file is one ruleset, named after the file. A rule's full name is <ruleset>_<rule name>, e.g. rule TELCO_002 in payments.zrules is payments_TELCO_002. Rules evaluate in alphabetical order of their full names, and when a whole ruleset is matched, every matching rule contributes its then assignments, with later rules overriding earlier ones for the same target.