---
title: Writing routing conditions
description: Use Tera template expressions in routing rule conditions to control which notifications are routed to which destinations based on alert names, labels, case status, and entity type.
url: https://coralogix.com/docs/user-guides/notification-center/routing/condition-templating/
product: notifications
feature: Notification Center
feature_id: notification-center
feature_identifier: notifications/notification-center
last_updated: '2026-11-03'
---

# Writing routing conditions

## Use Tera templates in routing rule conditions

A routing rule condition is a Tera expression that must evaluate to `true` or `false`.

The system automatically wraps your input with `{{ }}`, so provide only the expression itself.

```
alertDef.name is starting_with("CPU")
```

This expression evaluates to `true` when the alert name starts with "CPU".

## What a condition can reference

Every routing rule carries its own entity type, and the notifications processor only evaluates a rule against notifications of that type. A cases rule never sees an alert, and an alerts rule never sees a case.

| Entity type | Objects available                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------------------- |
| Alerts      | `alert.*`, `alertDef.*`, `_context.*`, and the flat shortcuts `correlationKey`, `name`, `priority`    |
| Cases       | `case.*`, `caseMetadata.*`, `_context.*`, and the flat shortcuts `correlationKey`, `name`, `priority` |

Each shortcut points at a different path on each entity type. Watch `priority` in particular: on an alert it is the highest priority across the firing's groups, not the priority set on the definition.

| Shortcut         | On an alert             | On a case       |
| ---------------- | ----------------------- | --------------- |
| `correlationKey` | `alert.groupingKey`     | `case.id`       |
| `name`           | `alertDef.name`         | `case.title`    |
| `priority`       | `alert.highestPriority` | `case.priority` |

Referencing the other entity type's paths fails in two different ways. In a cases rule the condition is rejected when you save it, because cases conditions are checked against an allow-list. In an alerts rule it saves without complaint and then never matches, because alerts have no allow-list and a path that is not there passes validation. See [Missing variables](#missing-variables).

See the real payload for your own data

To list every field a given notification actually carries, put this in a preset template and send a test notification. It prints the whole object, so you can read the exact paths from your own data rather than working from a reference.

```
{{ get_context() | json_encode(pretty = true) }}
```

### `alertDef.*`: the alert definition

The alert as it is configured, which is what most routing rules match on.

| Path                                                   | Holds                                                                         | Example value                |
| ------------------------------------------------------ | ----------------------------------------------------------------------------- | ---------------------------- |
| `alertDef.id`                                          | Alert definition ID                                                           | `9d4c1a0e-...`               |
| `alertDef.alertVersionId`                              | Version ID of the definition                                                  | `ver12345-...`               |
| `alertDef.name`                                        | Alert name                                                                    | `CPU Usage Alert`            |
| `alertDef.description`                                 | Alert description                                                             | `Fires when CPU exceeds 90%` |
| `alertDef.priority`                                    | Priority, `P1` to `P5`                                                        | `P1`                         |
| `alertDef.type`                                        | Alert type                                                                    |                              |
| `alertDef.entityLabels`                                | Labels on the definition, read by key                                         | `alertDef.entityLabels.team` |
| `alertDef.groupByKeys`                                 | Keys the alert groups by                                                      | `host`, `region`             |
| `alertDef.incidentSettings.notifyOn`                   | When the alert notifies                                                       | `Triggered`                  |
| `alertDef.incidentSettings.retriggeringPeriod.minutes` | Minutes before the alert can retrigger. A number.                             | `10`                         |
| `alertDef.typeDefinition`                              | The per-type configuration, which differs by `alertDef.type`                  |                              |
| `alertDef.createdTime` / `alertDef.updatedTime`        | When the definition was created and last changed. Epoch numbers, not strings. | `1768377731`                 |

Confirm the exact form of `alertDef.type` from a test notification before you match on it, using the tip above. A rule written against the wrong form silently never matches.

The two alert identifiers are easy to confuse

`alertDef.id` is the alert's permanent definition ID, and `alertDef.alertVersionId` identifies one version of that definition. To match the same alert across edits, you almost certainly want `alertDef.id`.

### `alert.*`: the firing instance

The specific firing, rather than the definition.

| Path                                    | Holds                                             | Example value                       |
| --------------------------------------- | ------------------------------------------------- | ----------------------------------- |
| `alert.id`                              | ID of this firing                                 | `76c411be-...`                      |
| `alert.status`                          | Status of the firing                              | `Triggered`                         |
| `alert.highestPriority`                 | Highest priority across the alert's groups        | `P1`                                |
| `alert.hasNoData`                       | Whether the alert is in a no-data state           | `false`                             |
| `alert.timestamp`                       | When it triggered. An epoch number, not a string. | `1768377731`                        |
| `alert.statusBreakdown.totalGroupCount` | How many groups the alert has                     | `12`                                |
| `alert.groupingKey`                     | Deduplication key                                 | `checkout:eu-west-1`                |
| `alert.alertDataUrl`                    | Deep link to the alert in Coralogix               | `https://...`                       |
| `alert.groups[0].keyValues["key"]`      | Group-by values for one group                     | `alert.groups[0].keyValues["host"]` |

Read a group-by key with brackets. Dot notation reads each dot as a nested level, so a key that contains dots, as `coralogix.metadata.subsystemName` does, is not found and the rule does not match:

```
alert.groups[0].keyValues["coralogix.metadata.subsystemName"]
```

For the full alert payload, see [Alerts notification source type schema](https://coralogix.com/docs/user-guides/alerting/configure-notifications/source-type-schema.md).

### `case.*` and `caseMetadata.*`

Cases carry a larger object than alerts do, but a routing condition may reference only part of it. Saving a router checks the condition against an allow-list, and a condition that reads anything outside it is rejected as invalid.

| Path                              | Holds                       | Values                                                                                                    |
| --------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------- |
| `case.id`                         | Case ID                     |                                                                                                           |
| `case.title`                      | Case title                  |                                                                                                           |
| `case.status`                     | Case status                 | `PENDING_ACTIVATION`, `ACTIVE`, `ACKNOWLEDGED`, `RESOLVED`, `CLOSED`                                      |
| `case.priority`                   | Priority                    | `P1` to `P5`                                                                                              |
| `case.category`                   | Category                    | `AVAILABILITY`, `SECURITY`                                                                                |
| `caseMetadata.notificationReason` | Why this notification fired | See [Cases entity type](https://coralogix.com/docs/user-guides/notification-center/entity-types/cases.md) |

Those paths, the flat shortcuts, and `_context.*` are the whole list. Everything else a case carries, including `case.labels`, `case.groupings`, and the rest of `caseMetadata.*`, works in a notification template but not in a routing condition: `case.labels["team"][0] == "platform"` is rejected when you save the router. The full template payload is listed in [Cases template variable reference](https://coralogix.com/docs/user-guides/notification-center/entity-types/cases.md#template-variable-reference).

### `_context.*`: the notification envelope

Shared by every entity type. Full list in [Dynamic templating](https://coralogix.com/docs/user-guides/notification-center/dynamic-templating.md#global-context-available).

| Path                     | Holds                                                                                                                  |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `_context.entityType`    | `alerts` or `cases`                                                                                                    |
| `_context.entitySubType` | The subtype, such as `metricThresholdMoreThanTriggered`                                                                |
| `_context.entityLabels`  | Routing labels on the notification. Each value is a list, so read it with `[0]`, as in `_context.entityLabels.team[0]` |
| `_context.system.name`   | Your Coralogix team name                                                                                               |
| `_context.trigger.type`  | `manual` or `automatic`                                                                                                |

## Alerts

### Condition for alert name starting with certain text

```
alertDef.name is starting_with("CPU")
```

| Alert name        | Result |
| ----------------- | ------ |
| CPU usage high    | true   |
| CPU saturation    | true   |
| Memory usage high | false  |

### Condition for alert name containing certain text

```
alertDef.name is containing("CPU")
```

| Alert name         | Result |
| ------------------ | ------ |
| CPU usage high     | true   |
| Database CPU spike | true   |
| Memory usage high  | false  |

### Condition for alert name matching a regex

```
alertDef.name is matching("(?i)^cpu")
```

* `^cpu` matches alert names starting with "cpu"
* `(?i)` makes the match case-insensitive

| Alert name        | Result |
| ----------------- | ------ |
| CPU usage high    | true   |
| cpu saturation    | true   |
| Memory usage high | false  |

### Condition for alert containing a specific label

```
alertDef.entityLabels.group is defined
```

| Labels            | Result |
| ----------------- | ------ |
| `group: payments` | true   |
| label not present | false  |

This condition is useful when routing alerts based on ownership labels such as `team`, `group`, or `service`.

### Condition for alert containing a specific value for a label

```
alertDef.entityLabels.environment == "production"
```

| Environment label | Result |
| ----------------- | ------ |
| production        | true   |
| staging           | false  |

Use this rule to route production alerts separately from staging or development alerts.

## Cases

Routing rules can also evaluate properties from the Cases payload when the entity type is `cases`.

Cases represent incidents created from alerts and move through the lifecycle states `PENDING_ACTIVATION`, `ACTIVE`, `ACKNOWLEDGED`, `RESOLVED`, and `CLOSED`. The payload is accessible through the `case` object.

### Condition for case status

Route notifications only while a Case is live.

```
case.status == "ACTIVE"
```

| Case status | Result |
| ----------- | ------ |
| ACTIVE      | true   |
| RESOLVED    | false  |
| CLOSED      | false  |

Typical use: send notifications while an incident is still live, and stay quiet once it is resolved.

### Condition for acknowledged cases

Send notifications only when a case has been acknowledged.

```
case.status == "ACKNOWLEDGED"
```

| Case status  | Result |
| ------------ | ------ |
| ACKNOWLEDGED | true   |
| ACTIVE       | false  |

Typical use: notify stakeholders when an incident has been acknowledged by an engineer.

### Combine case conditions

Route notifications only for acknowledged critical cases.

```
case.status == "ACKNOWLEDGED"

and case.priority == "P1"
```

| Status       | Priority | Result |
| ------------ | -------- | ------ |
| ACKNOWLEDGED | P1       | true   |
| ACKNOWLEDGED | P3       | false  |
| ACTIVE       | P1       | false  |

Typical use: escalate acknowledged critical incidents.

## Entity-type specific routing

A routing rule is already scoped to one entity type, and the notifications processor only evaluates it against notifications of that type. A cases rule never sees an alert notification, so a condition does not need to check for one:

```
_context.entityType == "cases"
```

In a routing rule this adds nothing to the filtering the rule's own entity type already does.

## How conditions differ from other templates

### Boolean only

Regular templates render text, while routing rule conditions must return a boolean value (`true` or `false`).

Invalid:

```
"CPU alert"
```

Valid:

```
alertDef.name is containing("CPU")
```

### Do not include {{}}

The system automatically wraps your expression.

Incorrect:

```
{{ alertDef.name is containing("CPU") }}
```

Correct:

```
alertDef.name is containing("CPU")
```

### Missing variables

If a condition references a path that does not exist, the condition fails to evaluate and the rule does not match. This applies to every operator and test, including `==`, `!=`, `in`, `is containing` and `is matching`.

```
alertDef.entityLabels.team == "payments"
```

If the `team` label is missing, this rule does not match.

A missing path is not treated as `false` inside the condition, so negations and `or` do not behave as you might expect. With no `team` label, neither of these rules matches:

```
alertDef.entityLabels.team != "payments"
```

```
alertDef.entityLabels.team == "payments" or alertDef.name is containing("CPU")
```

Guard the path with `is defined`. `and` and `or` stop evaluating once the result is known, so put the guard first:

```
alertDef.entityLabels.team is defined

and alertDef.entityLabels.team is containing("pay")
```

```
alertDef.entityLabels.team is not defined

or alertDef.entityLabels.team != "payments"
```

Matching against a list is the shorter way to write several alternatives, but it needs the same guard:

```
alertDef.entityLabels.team is defined

and alertDef.entityLabels.team in ["payments", "checkout"]
```

## Matching syntax

### Comparison operators

| Operator          | Means                  | Example                                                     |
| ----------------- | ---------------------- | ----------------------------------------------------------- |
| `==`              | Equal                  | `case.priority == "P1"`                                     |
| `!=`              | Not equal              | `alertDef.priority != "P5"`                                 |
| `>` `>=` `<` `<=` | Numeric comparison     | `alertDef.incidentSettings.retriggeringPeriod.minutes > 10` |
| `in`              | Value is one of a list | `alertDef.priority in ["P1", "P2"]`                         |

Use `==` and `!=` with any value type. The four ordering operators work only on fields that hold a number. Priority is a string (`"P1"` through `"P5"`), so compare it with `==` or `!=`, and list the ones you want rather than reaching for `<=`.

String comparison is case-sensitive and exact. To match part of a value, or to ignore case, use a test instead.

### Logical operators

| Operator | Means            | Example                                                  |
| -------- | ---------------- | -------------------------------------------------------- |
| `and`    | Both sides true  | `case.status == "ACTIVE" and case.priority == "P1"`      |
| `or`     | Either side true | `alertDef.priority == "P1" or alertDef.priority == "P2"` |
| `not`    | Inverts          | `not alertDef.name is containing("test")`                |

`and` binds tighter than `or`, so group with parentheses when you mix them:

```
(case.priority == "P1" or case.priority == "P2") and case.status == "ACTIVE"
```

Without the parentheses that reads as `P1`, or `P2` and active, which is not the same rule.

### Tests

A test is written `value is test_name(argument)` and returns a boolean.

| Test            | True when                                  | Example                                                          |
| --------------- | ------------------------------------------ | ---------------------------------------------------------------- |
| `defined`       | The path exists in the payload             | `alertDef.entityLabels.team is defined`                          |
| `undefined`     | The path is absent                         | `alertDef.entityLabels.team is undefined`                        |
| `starting_with` | The string starts with the argument        | `alertDef.name is starting_with("CPU")`                          |
| `ending_with`   | The string ends with the argument          | `alertDef.name is ending_with("prod")`                           |
| `containing`    | The string contains the argument           | `alertDef.name is containing("CPU")`                             |
| `matching`      | The string matches a regular expression    | `alertDef.name is matching("(?i)^cpu")`                          |
| `string`        | The value is a string                      | `case.title is string`                                           |
| `number`        | The value is a number                      | `alertDef.incidentSettings.retriggeringPeriod.minutes is number` |
| `odd`           | The number is odd                          | `alert.statusBreakdown.totalGroupCount is odd`                   |
| `even`          | The number is even                         | `alert.statusBreakdown.totalGroupCount is even`                  |
| `divisibleby`   | The number divides exactly by the argument | `alert.statusBreakdown.totalGroupCount is divisibleby(5)`        |
| `iterable`      | The value is an array or an object         | `alertDef.groupByKeys is iterable`                               |
| `object`        | The value is an object                     | `alertDef.entityLabels is object`                                |

That is the whole set. A name that is not on this list is not a test, and the rule is rejected when you save it.

`starting_with`, `ending_with`, and `containing` are case-sensitive. For a case-insensitive match, use `matching` with the `(?i)` flag.

`defined` and `undefined` are the only two tests that tolerate a missing path, which is what makes them usable as guards. Every other test on this list fails on one, and the rule is skipped. `odd`, `even`, and `divisibleby` fail the same way when the value is not a number.

Negate a test with `is not`:

```
alertDef.name is not containing("test")
```

## Regular expressions

`is matching` takes a regular expression, written as a string.

### Backslashes and quotation marks

A condition string carries no escape sequences. Everything between the opening and closing delimiter reaches the expression exactly as you typed it, so a backslash is an ordinary character. Write it once:

```
alertDef.name is matching("^prod-\d+$")
```

Doubling it sends a literal backslash to the regular expression, which then matches the two characters `\d` rather than a digit, and the rule silently never matches.

For the same reason a string cannot contain its own delimiter, and there is no way to escape one. A condition string may be wrapped in double quotation marks, single quotation marks, or backticks, so use a delimiter the value does not contain:

```
alertDef.name is containing('a "quoted" word')
```

### Matching is partial unless you anchor it

A pattern matches when it is found anywhere in the value. Anchor it to control that:

| Pattern   | Matches                      |
| --------- | ---------------------------- |
| `"cpu"`   | Any name containing `cpu`    |
| `"^cpu"`  | Any name starting with `cpu` |
| `"cpu$"`  | Any name ending with `cpu`   |
| `"^cpu$"` | The exact name `cpu`         |

### Supported syntax

| Syntax                   | Meaning                               | Example             |
| ------------------------ | ------------------------------------- | ------------------- |
| `^` `$`                  | Start and end of the string           | `^prod-`            |
| `.`                      | Any character                         | `a.c`               |
| `[abc]` `[^abc]` `[a-z]` | Character class, negated class, range | `[0-9]+`            |
| `[0-9]` `[A-Za-z]`       | Digit, letter                         | `[0-9]+`            |
| `\d` `\w` `\s`           | Digit, word character, whitespace     | `^prod-\d+$`        |
| `*` `+` `?`              | Repetition                            | `prod-.*`           |
| `{2}` `{2,}` `{2,5}`     | Counted repetition                    | `[a-z]{3}`          |
| `\|`                     | Alternation                           | `^(prod\|staging)-` |
| `(...)`                  | Group                                 | `(prod)-api`        |
| `(?:...)`                | Non-capturing group                   | `(?:prod\|staging)` |
| `(?i)`                   | Case-insensitive from this point      | `(?i)^cpu`          |
| `(?s)`                   | Dot matches newlines                  | `(?s).+`            |
| `(?m)`                   | `^` and `$` match at line breaks      | `(?m)^error`        |

### Not supported

The following constructs are not supported:

| Not supported  | Example of what fails |
| -------------- | --------------------- |
| Lookahead      | `^(?!test-)`          |
| Lookbehind     | `(?<=prod-)api`       |
| Backreferences | `([a-z]+)-\1`         |

An expression using any of these fails to compile, so the rule is rejected when you save it. Rewrite it with the supported syntax: a negative lookahead is usually better expressed as `not alertDef.name is starting_with("test-")`.

## Next steps

Create and manage connectors that link Coralogix to third-party services in [Connectors](https://coralogix.com/docs/user-guides/notification-center/connectors.md).
