Skip to content

Documentation

Rule operators

All eighteen rule operators, what each one tests, and the exact reason string it returns when it matches and when it does not.

A condition is three things: a field path into the event payload, an operator, and zero or more values to compare against. The operator decides what comparison happens and what the evaluation says afterwards.

There are eighteen. Every one of them returns a reason whether it matches or not — that is the point of the engine, and the reason strings below are the product's own, character for character.

The operators

Operator Values What it tests
EQUALS one The field equals the value
NOT_EQUALS one The field does not equal the value
IN many The field is one of the values
NOT_IN many The field is none of the values
CONTAINS one The field contains the value as a substring
NOT_CONTAINS one The field does not contain the value
STARTS_WITH one The field begins with the value
ENDS_WITH one The field ends with the value
GT one The field is numerically greater than the value
GTE one The field is greater than or equal to the value
LT one The field is numerically less than the value
LTE one The field is less than or equal to the value
EXISTS none The field is present
NOT_EXISTS none The field is absent
IS_TRUE none The field is truthy
IS_FALSE none The field is not truthy
MATCHES_REGEX one The field matches an RE2 regular expression
TRIGGER_MATCH none Another trigger fires for this same event

Operators taking "many" read the whole values list. Operators taking "one" read the first value and ignore the rest.

Reason strings

This is what the evaluation tells you. %q renders a quoted string and %v renders a list as [a b c], both following Go's formatting.

Operator Matched Did not match
EQUALS value "x" equals "x" value "x" does not equal "y"
NOT_EQUALS not_equals: value "x" does not equal "y" not_equals: value "x" equals "x"
IN value "x" found in [x y] value "x" not found in [y z]
NOT_IN not_in: value "x" not found in [y z] not_in: value "x" found in [x y]
CONTAINS value "abc" contains "b" value "abc" does not contain "z"
NOT_CONTAINS not_contains: value "abc" does not contain "z" not_contains: value "abc" contains "b"
STARTS_WITH value "abc" starts with "a" value "abc" does not start with "z"
ENDS_WITH value "abc" ends with "c" value "abc" does not end with "z"
GT 5 GT 3 5 not GT 9
GTE 5 GTE 5 5 not GTE 9
LT 3 LT 5 9 not LT 5
LTE 5 LTE 5 9 not LTE 5
EXISTS field exists field does not exist
NOT_EXISTS field does not exist field exists
IS_TRUE value true is_true=true value false is_true=false
IS_FALSE value false is_false=true value true is_false=false
MATCHES_REGEX value "a1" matches regex "^a\d$" value "bb" does not match regex "^a\d$"
TRIGGER_MATCH the referenced trigger's own reason the referenced trigger's own reason

Two of these repay a second look.

Negated operators keep the positive form's reason and prefix it. A NOT_IN that matches still reads not found in, because what it is reporting is the comparison it ran, not the verdict it reached. Once you expect it, it is the more informative of the two.

EXISTS and NOT_EXISTS describe the field, not the outcome. Both return field exists or field does not exist depending on what is actually there, so NOT_EXISTS matching reads field does not exist.

Arrays

When a field path crosses a JSON array of objects, the values from every element are collected and the operator is applied with any-match semantics: the condition matches if any element matches. EXISTS and NOT_EXISTS are the exceptions and test the field itself.

The reason names which behaviour ran, and counts what it looked at:

array element matched: value "terminated" equals "terminated"
no array element matched (checked 3 elements)

Composing triggers

TRIGGER_MATCH is the one operator that does not compare a field. It references another trigger and matches when that trigger fires for the same event, which is how a condition reuses logic you have already written rather than restating it.

It never reaches the comparison code — the evaluator intercepts it, evaluates the referenced trigger, and memoises the result for the rest of the message. A condition satisfied from that cache reports:

result from memoization cache

A reference that loops back to a trigger already being evaluated, including a trigger that references itself, doesn't match:

ref trigger 42 is already being evaluated: TRIGGER_MATCH cycle, treated as not matched

Past 32 nested references, the next one isn't evaluated:

TRIGGER_MATCH nesting exceeds 32; ref trigger 42 not evaluated

When a condition cannot be evaluated

These come back as not matched, with a reason saying why rather than failing the run:

Reason Cause
field is nil The field path resolved to nothing
no comparison value configured The operator needs a value and none was set
no regex pattern configured MATCHES_REGEX with no pattern
cannot parse field value abc as number: ... A numeric operator on a non-numeric field
cannot parse comparison value "abc" as number: ... A numeric operator with a non-numeric value
invalid regex "(" : ... MATCHES_REGEX with a pattern RE2 rejects

There is one more you should never see, and it is worth knowing on sight:

provider scope 4 does not match current provider 7; skipped

A condition scoped to a provider that did not send this event is skipped, and counts as matched, so that a rule written for one provider does not veto events from another. Triggers and rules explains why.

How values are compared

Comparisons are made on the string form of the field value. The numeric operators — GT, GTE, LT, LTE — parse both sides as numbers at the point of comparison and report a parse failure as the reason if either side will not convert.

IS_TRUE and IS_FALSE treat the following as true: the boolean true, the strings true, 1 and yes in any case, and any non-zero number. Everything else, including the empty string, 0 and false, is not truthy.

A condition also carries a value_type of STRING, NUMBER or BOOLEAN. It records what you meant the field to be, and the rule builder uses it to pick a sensible input — but the comparison itself does not branch on it. The operator you choose is what determines how the values are compared.