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 formAliasExample
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 ofincategory is one of 'fixed', 'variable'
is not one ofnot inchannel 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 between takes two values joined by and, and both boundaries are inclusive - amount is between 100 and 1000 matches 100 and 1000 themselves
  • matches compares against a regular expression anchored at the start of the value - doc_id matches 'INV-' matches INV-123 but not 2024-INV-123. The pattern must be a quoted literal
  • is true and is false need 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:

when
    transaction_type is not customer_type and
    order.amount is more than limits.daily_max

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
    status is one of 'new', 'pending'
    status in ['new', 'pending']
    status in 'new', 'pending'

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:

when
    tier is one of allowed_tiers

Blocks

A rule is made of blocks, each keyword alone on its own line with the content indented under it:

BlockRequiredContent
ruleYesThe rule's name, one line, letters, digits and underscores
docsNoFree-form documentation text, any number of lines
defaultsNoname = value lines with concrete values
whenYesConditions, one per line
thenYestarget = value lines applied when the rule matches
elseNotarget = 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:

when
    flight_delay is more than 180 or
    is_cancelled is true

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" or r'raw text'. Regular expression patterns work unescaped, so '\d{4}' means four digits
  • Numbers - integers and floats
  • Booleans - true and false
  • 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:

when
    # Only transactions above the reporting threshold
    amount is more than 10000

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:

Rule Payments_001 cannot run - the input has no value for 'credit_score'

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.