# 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](https://zato.io/docs/rule-engine/tutorial.html), and for guidance on writing fast rules, read about [performance](https://zato.io/docs/rule-engine/performance.html).

## Operators {#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 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:

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

## List membership {#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:

```zrules
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:

```zrules
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:

```zrules
when
    tier is one of allowed_tiers
```

## Blocks {#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:

```zrules
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-and-logic}

Conditions live in the `when` block, one per line, and every line except the last ends with `and` or `or`:

```zrules
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 {#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}

Comments start with `#` and run to the end of the line, anywhere outside a quoted string:

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

## Missing fields raise errors {#missing-fields-raise-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:

```text
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 {#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.
