Skip to main content

Predicates

A predicate is a recursive boolean expression that defines audience membership. Predicates appear in two places:

  • [segment.predicate] inside segment files — the original home, reusable across flags.
  • predicate = { ... } inside a flag rule — inline, convenient for one-off audiences. See flag § Inline predicates.

The shape, operators, and validation rules below apply to both sites identically. A flag rule MAY use segment = "..." instead of an inline predicate; a single rule MUST declare exactly one of segment or predicate (mixing both raises E036).


Operator quick reference

The full operator set, one line per operator. See § Per-operator notes for behavior details.

OperatorOperandOperand typeAttribute typeOne-line semantics
eqvaluescalarstring / int / float / boolAttribute equals operand.
neqvaluescalarstring / int / float / boolAttribute does not equal operand.
gtvaluenumberint / floatAttribute strictly greater than operand.
gtevaluenumberint / floatAttribute ≥ operand.
ltvaluenumberint / floatAttribute strictly less than operand.
ltevaluenumberint / floatAttribute ≤ operand.
invaluesnon-empty scalar arraystring (or matching scalar)Attribute equals one of the values.
not_invaluesnon-empty scalar arraysame as inAttribute equals none of the values.
containsvaluestringstringSubstring match. Case-sensitive.
not_containsvaluestringstringSubstring non-match. Case-sensitive.
starts_withvaluestringstringPrefix match. Case-sensitive.
ends_withvaluestringstringSuffix match. Case-sensitive.
semver_eqvaluestring (semver)string (semver)Attribute semver == operand semver.
semver_gtvaluestring (semver)string (semver)Attribute semver > operand semver.
semver_gtevaluestring (semver)string (semver)Attribute semver ≥ operand semver.
semver_ltvaluestring (semver)string (semver)Attribute semver < operand semver.
semver_ltevaluestring (semver)string (semver)Attribute semver ≤ operand semver.
is_setanyAttribute key present in the evaluation context.
is_not_setanyAttribute key absent.

The set is closed. Operator names are byte-exact case-sensitive (op = "EQ" is E015). The set-typed operators (set_contains / set_not_contains), the string_set attribute type, and matches_regex are reserved for a future minor version.


Predicate forms

Three top-level forms are valid:

FormTOML shapeExample
Attribute atominline table with attribute, op, and value (or values){ attribute = "user.country", op = "eq", value = "US" }
Segment referenceinline table with segment{ segment = "beta-users" }
Compound expressioninline table whose key is and, or, or not; value is an array (for and / or) or a sub-predicate (for not){ and = [ … ] }

Compound expressions can nest other compound expressions, atoms, and segment references to arbitrary depth. The linter emits W005 when nesting exceeds 5 levels.


Attribute atoms

An attribute atom tests a single attribute from the evaluation context against an operand.

{ attribute = "<dot.path>", op = "<operator>", value = <operand> }

or, for operators that take a list:

{ attribute = "<dot.path>", op = "<operator>", values = [<operand>, <operand>, ...] }
FieldTypeRequiredDescription
attributestringyes (absent → E015)Dot-separated path into the evaluation context's attributes. The leading prefix (user, app, …) is conventional but not enforced.
opstringyesOperator name.
valuescalar (string / int / float / bool)depends on operatorSingle operand for scalar / comparison / string / semver operators.
valuesarray of scalarsdepends on operatorOperand list for in / not_in.

The linter validates atom shape via E015:

  • op MUST be a string and a known operator name.
  • The operand field (value vs. values) MUST be the one the operator expects.
  • The operand's TOML type MUST match the operator's signature.
  • Operators that take no operand (is_set, is_not_set) MUST NOT carry a value or values field.

Unknown sibling keys on an atom table (anything other than attribute / op / value / values) produce E016. Mixing atom keys with and / or / not / segment at the same level remains E015 (ambiguous shape).

Attribute paths

Attribute paths are literal keys into the flat evaluation-context attribute map. The dot is a convention for readability; the SDK does NOT split on it or look up nested objects. An atom whose attribute is "user.country" matches the context entry whose key is exactly the string "user.country".

See evaluation-context for the conventional prefixes (user.*, app.*, org.*, …) and missing-attribute behavior.


Per-operator notes

The quick reference above covers the basic shape. Operators with non-obvious behavior:

in / not_in — operand requirements

The values array MUST contain at least one entry; values = [] produces E033. An empty in would always evaluate to false and an empty not_in to true; both forms are degenerate and almost always authoring oversights.

The array's entries MUST all be scalars (string, integer, float, or boolean); a non-scalar entry (table or array) produces E015. Arrays MAY mix scalar types (e.g., [1, "1"]) — the runtime comparison is per-entry against the attribute value following the same type rules as eq — but uniform-type arrays are strongly recommended for readability.

Substring operators with empty-string operand

contains, not_contains, starts_with, and ends_with with value = "" are mathematically degenerate — every string contains, starts with, and ends with the empty string. An author writing such an atom is almost always making a mistake (forgot to fill in the operand, or copied a placeholder). The linter raises W015. To express a true-everywhere atom intentionally, use op = "is_set" against a known-present attribute.

Case sensitivity and Unicode

All string operators are case-sensitive and byte-exact. Comparison runs on the raw UTF-8 byte sequences: no Unicode normalization (NFC, NFD, NFKC, NFKD), no case folding, no locale-aware collation, no whitespace trimming.

Two strings that look identical to a human but use different Unicode encodings (e.g., precomposed é U+00E9 vs. decomposed e + U+0301) are not equal under eq. SDKs and the server MUST compare bytes verbatim.

To do a case-insensitive or normalized comparison, normalize the attribute on the SDK side before evaluation, or define a derived attribute (user.country_lower).

Numeric widening on eq / neq / comparison

The numeric operators (gt, gte, lt, lte) and eq / neq accept either integer or float on either side. When attribute and operand differ in numeric type, both sides are widened to IEEE 754 double for the comparison. eq between 1 (integer attribute) and 1.0 (float operand) is true.

Pin the same numeric type on both sides when authoring rules. Widening exists to make obvious cross-type comparisons work, not as license to mix freely.

Type mismatch at evaluation time

If the runtime attribute type does not match what the operator requires (e.g., gt against a string attribute), the predicate evaluates to false. This is not an error; the segment simply does not match. SDKs MAY log a warning when an explicit context schema is configured; with no schema, the mismatch is silent.

The linter does its own static check across files: an attribute used under operator/operand pairs that imply different types raises E034. See evaluation-context § Inferred attribute types.

Missing-attribute behavior

If the predicate's attribute is absent from the evaluation context:

  • is_set returns false; is_not_set returns true.
  • All other operators return false.

Missing attributes never cause evaluation to error.

Semver parsing

Semver operators expect both attribute value and operand to be valid semver 2.0 strings. The linter validates the operand string at lint time and emits E015 if it is not parseable. The attribute side is dynamic and validated at evaluation time: an attribute that fails to parse as semver causes the atom to evaluate to false.


Segment references

A predicate atom of the form { segment = "<key>" } tests whether the entity is a member of the named segment. Segment references can appear at any position in a predicate tree.

[segment.predicate]
or = [
{ attribute = "user.country", op = "eq", value = "US" },
{ segment = "beta-users" },
]

The referenced segment is evaluated recursively against the same evaluation context. If the referenced segment defines a bucket, the bucket's entity_id_attribute is read from the same context.

Validation rules

  • The referenced key MUST exist as a segment file in the flag namespace; otherwise E005.
  • Segment references MUST NOT participate in cycles; otherwise E012.
  • Segment references appearing inside and / or / not are still validated.
  • A segment reference is not an attribute atom and therefore not subject to E015 operator validation.

Negation

{ not = { segment = "beta-users" } }

Negates segment membership. Equivalent to "the entity is not in beta-users".


Compound expressions

Three boolean combinators compose predicates into trees: and, or, not.

and

{ and = [
{ attribute = "user.country", op = "eq", value = "US" },
{ attribute = "user.plan", op = "in", values = ["pro", "enterprise"] }
]}
  • TOML shape: a table with key and whose value is an array of sub-predicates. A non-array value raises E015.
  • Semantics: true iff every sub-predicate is true.
  • Short-circuit: stops at the first false in declared order.
  • Each array entry MUST be a TOML table. A non-table entry such as and = [42, "x"] raises E015.

An empty and = [] is vacuous truth (true). The linter emits W007 so the empty list is an explicit choice rather than an oversight.

or

{ or = [
{ attribute = "user.plan", op = "eq", value = "pro" },
{ segment = "beta-users" }
]}
  • TOML shape: a table with key or whose value is an array of sub-predicates.
  • Semantics: true iff any sub-predicate is true.
  • Short-circuit: stops at the first true in declared order.

An empty or = [] is false. The linter emits W007.

not

{ not = { attribute = "user.plan", op = "eq", value = "free" } }
  • TOML shape: a table with key not whose value is a single sub-predicate (not an array). An array or scalar value raises E015.
  • Semantics: boolean negation of the wrapped predicate.

A not whose body is itself not { ... } is permitted but discouraged; double negation reads poorly.

Unknown sibling keys on a compound table

A compound table MUST contain exactly one of and, or, not. Any sibling key on the same table — { and = [...], extra = "x" }, or two compound keys at once like { and = [...], or = [...] } — produces E016. The same applies to segment-reference tables: { segment = "x", extra = "y" } raises E016.

Mixing and nesting

Combinators may be mixed and nested:

{ and = [
{ attribute = "user.country", op = "eq", value = "US" },
{ or = [
{ attribute = "user.plan", op = "eq", value = "pro" },
{ segment = "beta-users" }
]}
]}

The linter measures nesting depth as the longest chain of and / or / not keys from the root. A predicate exactly 5 levels deep does NOT trigger W005; 6 levels deep does. Atom tables and segment-reference tables do not count as a level — only and / or / not keys increment the depth.


Top-level predicate shapes

The [segment.predicate] table can be written as:

Atom directly under the table

[segment.predicate]
attribute = "user.country"
op = "eq"
value = "US"

A single attribute atom inlined at the predicate root.

Compound at the root

[segment.predicate]
and = [
{ attribute = "user.country", op = "eq", value = "US" },
{ attribute = "user.plan", op = "in", values = ["pro", "enterprise"] }
]

Segment reference at the root

[segment.predicate]
segment = "beta-users"

A [segment.predicate] MUST declare exactly one of these three shapes. Mixing atom keys (attribute / op) with compound keys (and / or / not) or with segment at the same table level produces an ambiguous shape and is rejected with E015.

An empty [segment.predicate] table (no keys at all — neither atom keys, nor compound keys, nor segment) is rejected with E015. A segment that needs to match every entity should use a tautology atom (e.g., attribute = "user.id", op = "is_set"); a segment that needs to match no entity should use a negation of such a tautology.


Worked examples

"US users on the pro or enterprise plan"

[segment.predicate]
and = [
{ attribute = "user.country", op = "eq", value = "US" },
{ attribute = "user.plan", op = "in", values = ["pro", "enterprise"] }
]

"Beta cohort minus suspended users"

[segment.predicate]
and = [
{ segment = "beta-users" },
{ not = { attribute = "user.status", op = "eq", value = "suspended" } }
]

"Internal employees, OR users on a recent app version"

[segment.predicate]
or = [
{ segment = "internal-employees" },
{ attribute = "app.version", op = "semver_gte", value = "2.4.0" }
]

"Has the email attribute set, and the email contains the company domain"

[segment.predicate]
and = [
{ attribute = "user.email", op = "is_set" },
{ attribute = "user.email", op = "contains", value = "@example.com" }
]

Common diagnostics

ScenarioDiagnostic
op is not a stringE015
op is an unrecognized operator (including wrong-case forms)E015
Operator requires value but the atom has values (or vice versa)E015
Operator requires no operand but the atom has value or valuesE015
Operand type does not match operator's signatureE015
values array contains non-scalar entriesE015
values = [] on in / not_inE033
Atom omits attributeE015
Atom mixes atom keys with compound keys at the same levelE015
Atom contains an unknown sibling keyE016
[segment.predicate] table is emptyE015
Compound's and / or value is not an arrayE015
Compound's not value is an arrayE015
Compound's and / or array contains a non-table entryE015
Compound table contains a sibling key alongside and / or / notE016
Segment-reference table contains a sibling key alongside segmentE016
Semver operand fails to parseE015
Predicate references a missing segmentE005
Predicates form a cycleE012
Predicate nesting exceeds 5 levelsW005
Empty and = [] or or = []W007
Substring op with value = ""W015
Same attribute used under operator/operand pairs implying different typesE034

  • Read predicates top-to-bottom, left-to-right. Order the children of and / or so the most discriminating sub-predicate appears first; short-circuit returns earlier.
  • Prefer segment composition over deeply nested predicates. A segment whose predicate is or = [ { segment = "beta-users" }, { segment = "internal-employees" } ] is easier to read and maintain than a 6-deep predicate tree.
  • Always namespace attributes (user.id, app.version, device.os). Reviewers reading a predicate should be able to tell at a glance what the attribute represents.
  • Avoid mixing semver and string eq. A version comparison via eq will mis-order "2.10.0" versus "2.2.0" — use the semver operators when comparing version numbers.
  • Pin a segment for every audience your team uses more than once. Inline-only audiences create copy-paste drift.

See also

  • segment — where [segment.predicate] lives.
  • flag — inline predicate = { ... } on rules.
  • evaluation-context — attribute lookup, missing-attribute and type-mismatch semantics.
  • buckets[segment.bucket], the other half of segment membership.
  • diagnosticsE005, E012, E015, E016, E033, E034, W005, W007, W015.