Definitions in depth
Operators
An operator says how a condition compares two values, such as equals, in or before. The same operators work in segments, variable conditions and test checks. This page gives each one an example, grouped by what it compares, then explains what happens when a value is missing.
Write a condition#
A condition names an attribute from the context, an operator and the value to compare with:
description: The Netherlandsconditions: - attribute: country operator: equals value: nlThe same operators work in three places:
- Segment conditions, and inline
conditionsin force entries and overrides, compare anattribute. A dotted path such asaccount.seatsreads a property of an object attribute. - Variable conditions compare a
variable, optionally of afeature, with nested values read throughpath. - Test checks compare a result with a
value, or with another result throughcompareTo.
This requirement uses includes on a global variable's array:
# ...conditions: - variable: checkoutSettings path: paymentMethods operator: includes value: idealTo keep some operators out of a project, list them in disallowOperators. Linting and building then reject them.
Operators by type#
Every operator has its own example below, a complete segment you can copy. They use country and userId from the scaffold, and the plan, account, version, signupDate and permissions attributes declared in Attributes. Link to an operator's heading to point someone at its example.
Equality#
equals and notEquals compare a single value, such as a string, number, boolean or null.
equals#
description: The Netherlandsconditions: - attribute: country operator: equals value: nlMatches a context whose country is exactly nl. The comparison is case sensitive, so NL does not match.
notEquals#
description: Everyone outside the Netherlandsconditions: - attribute: country operator: notEquals value: nlMatches every other country. It also matches when the attribute is missing, so this segment includes every user whose context has no country. Use notIn, or add an exists condition, if that is not what you want.
Numbers#
greaterThan, greaterThanOrEquals, lessThan and lessThanOrEquals compare numbers, here the seats of the account object attribute.
greaterThan#
description: Accounts with more than 10 seatsconditions: - attribute: account.seats operator: greaterThan value: 10An account with 11 seats matches, and one with 10 does not.
greaterThanOrEquals#
description: Accounts with 50+ seatsconditions: - attribute: account.seats operator: greaterThanOrEquals value: 50An account with exactly 50 seats matches.
lessThan#
description: Accounts with fewer than 5 seatsconditions: - attribute: account.seats operator: lessThan value: 5An account with 4 seats matches, and one with 5 does not.
lessThanOrEquals#
description: Accounts with one seat at mostconditions: - attribute: account.seats operator: lessThanOrEquals value: 1An account with exactly one seat matches.
Text#
contains, notContains, startsWith and endsWith compare strings, here the locale of the account object attribute. All four are case sensitive.
contains#
description: Locales for the Netherlandsconditions: - attribute: account.locale operator: contains value: NLnl-NL and fy-NL match, and nl-BE does not.
notContains#
description: Locales outside Britainconditions: - attribute: account.locale operator: notContains value: GBen-US matches, and en-GB does not.
startsWith#
description: Dutch speaking accountsconditions: - attribute: account.locale operator: startsWith value: nl-nl-NL and nl-BE match, and fy-NL does not.
endsWith#
description: Locales for Belgiumconditions: - attribute: account.locale operator: endsWith value: -BEnl-BE and fr-BE match, and nl-NL does not.
Membership#
in and notIn check a value against a list.
in#
description: Paid and enterprise accountsconditions: - attribute: plan operator: in value: - paid - enterpriseMatches a context whose plan is any value in the list.
notIn#
description: Accounts on any plan but enterpriseconditions: - attribute: plan operator: notIn value: - enterpriseMatches free and paid. Unlike notEquals, it does not match a context without a plan.
In attribute conditions, both work on strings only, and the attribute must be declared as a string. For numbers, put equals conditions in an or group, and in place of notIn, wrap that group in not. Variable conditions also accept numbers and null in the list.
Arrays#
includes and notIncludes check whether an array attribute, here permissions, contains one item.
includes#
description: Accounts that can editconditions: - attribute: permissions operator: includes value: writeMatches [read, write], and not [read].
notIncludes#
description: Accounts without admin rightsconditions: - attribute: permissions operator: notIncludes value: adminMatches [read, write], and not [read, admin].
includesAny#
description: Accounts that can edit or administerconditions: - attribute: permissions operator: includesAny value: [write, admin]Matches [read, write] and [admin], but not [read].
notIncludesAny#
description: Accounts without editing rightsconditions: - attribute: permissions operator: notIncludesAny value: [write, admin]Matches [read] and [], but not [read, write].
Both list operators use strict matching. In these conditions, every candidate must be a string allowed by the permissions item enum. See array list comparisons for candidate types, empty arrays and invalid values.
Patterns#
matches and notMatches test a string against a regular expression. Add regexFlags for flags such as i.
matches#
description: Dutch or Belgian localesconditions: - attribute: account.locale operator: matches value: ^(nl|be)- regexFlags: iWith the i flag, nl-NL and NL-nl both match.
notMatches#
description: Locales in any language but Englishconditions: - attribute: account.locale operator: notMatches value: ^en-nl-NL matches, and en-GB does not.
Keep patterns simple so every SDK agrees. Regular expressions lists what is portable.
Dates#
before and after compare timestamps, here the signupDate attribute.
A string with format: date-time accepts only before, after, exists, notExists, isNull and notNull. This also applies to object properties, paths whose schema branches all use that format, and global variables with it. Ordinary string attributes can still use before and after with valid timestamps.
before#
description: Accounts created before 2026conditions: - attribute: signupDate operator: before value: 2026-01-01T00:00:00ZAn account created on 2025-12-31T23:59:59Z matches.
after#
description: Accounts created this yearconditions: - attribute: signupDate operator: after value: 2026-01-01T00:00:00ZAn account created on 2026-03-01T00:00:00Z matches.
Both sides need a time and a timezone. See Timestamps for the exact format.
Both comparisons are strict, so neither matches the exact boundary. To include a launch instant of 2026-11-27T00:00:00Z, use after with 2026-11-26T23:59:59.999Z. See Scheduled releases for a complete example.
Do not replace before with not around after. Missing or invalid date inputs make before and after return false in SDK conditions, which not can turn into true. It also includes the exact boundary. Presence checks do not validate a timestamp.
Versions#
Six operators compare semantic versions, here the app's version attribute, a string with format: semver. Lint rejects a value these operators cannot compare, such as 5.5,0, because the condition could never match.
semverEquals#
description: App version 5.5.0conditions: - attribute: version operator: semverEquals value: 5.5.05.5.0 matches, and so does 5.5, since missing parts count as zero.
semverNotEquals#
description: Every version but 5.4.1conditions: - attribute: version operator: semverNotEquals value: 5.4.1Leaves out one release, such as one with a known bug.
semverGreaterThan#
description: App versions after 5.5.0conditions: - attribute: version operator: semverGreaterThan value: 5.5.05.10.0 matches, because versions compare by number.
semverGreaterThanOrEquals#
description: App version 5.5.0 or laterconditions: - attribute: version operator: semverGreaterThanOrEquals value: 5.5.0A prerelease such as 5.5.0-beta.1 sorts before 5.5.0, so it does not match.
semverLessThan#
description: App versions before 5.0.0conditions: - attribute: version operator: semverLessThan value: 5.0.04.9.9 matches, and 5.0.0 does not.
semverLessThanOrEquals#
description: App version 5.4.0 or earlierconditions: - attribute: version operator: semverLessThanOrEquals value: 5.4.05.4.0 matches, and 5.4.1 does not.
See Semantic versions for the full ordering.
Presence and emptiness#
exists, notExists, isNull, notNull, empty and notEmpty take no value.
exists#
description: Signed in usersconditions: - attribute: userId operator: existsMatches any context with a userId, whatever its value.
notExists#
description: Visitors who are not signed inconditions: - attribute: userId operator: notExistsMatches a context without a userId.
isNull#
description: Users whose account is nullconditions: - attribute: account operator: isNullMatches when the application passes account: null, and not when it leaves account out.
notNull#
description: Users with an accountconditions: - attribute: account operator: notNullMatches any account that is present and not null.
empty#
description: Accounts without permissionsconditions: - attribute: permissions operator: emptyMatches permissions: [].
notEmpty#
description: Accounts with any permissionconditions: - attribute: permissions operator: notEmptyMatches [read], and not [].
Which values match#
The six operators side by side, for each kind of value:
| Value | exists | isNull | notNull | empty | notEmpty |
|---|---|---|---|---|---|
| Missing | No | No | No | No | No |
null | Yes | Yes | No | No | No |
"", [], {} | Yes | No | Yes | Yes | No |
" ", [null] | Yes | No | Yes | No | Yes |
A number or true | Yes | No | Yes | No | No |
notExists is the opposite of exists. Emptiness is shallow, so { x: null } is not empty. A missing variable result counts as null.
Missing values#
When the attribute or path is missing, most operators return false, including negative ones such as notIn, notContains and notMatches. Three things differ:
notEqualsreturnstrue, as described under Equality.notExistsreturnstrue.notaround another operator turns itsfalseintotrue. Sonotaroundemptymatches a missing value, whilenotEmptydoes not. Likewise,notNullrequires a value to be present, whilenotaroundisNulldoes not.
Test checks are stricter. With a missing path, every operator except the six presence and emptiness ones returns false, notEquals included.
Objects and arrays never compare by deep equality.
What linting checks#
Linting compares each operator with the declared types where it knows them:
inandnotInin conditions need astringattribute, including nested paths, and a list of strings.inandnotInvariable condition lists may contain strings, numbers andnull, but never booleans. Compare booleans withequalsornotEquals.includesAnyandnotIncludesAnyconditions need a string array attribute or path. Each candidate must be a string matching its declared item constraints, including enums. Forpermissions, this means onlyread,writeoradmin.- In
conditions, these two array list operators accept broader primitive lists: strings, numbers, booleans ornull, subject to the referenced variable's known item schema. Objects and nested arrays are rejected as candidates. - An operator that cannot work on the declared type is rejected, such as
greaterThanon a string oremptyon a number. - Date attributes reject equality, string and emptiness operators. Their
enumandconstvalues must be valid timestamps too. - Unsupported regular expression constructs are rejected.
Details#
Array list comparisons#
includesAny and notIncludesAny require an actual array and a candidate array of JSON primitives: strings, numbers, booleans or null. Authored attribute arrays contain strings only. Each candidate must also match the attribute's declared item type and constraints, including enums. Variable conditions accept broader primitive lists where the referenced schema permits them.
Matching is strict, so 1 does not match "1" and true does not match 1. Objects and nested arrays in context do not match primitive candidates. A nested object attribute path works too, if it resolves to an array.
With an empty actual array or an empty candidate array, includesAny returns false and notIncludesAny returns true. Missing, null or non array actual values make both return false at runtime.
A non array candidate value, or any object or nested array in the candidate list, also makes both return false, even if another candidate would match. Do not replace notIncludesAny with not around includesAny, since that would match invalid or missing values.
The same list comparisons work in required variables and test checks. Checks report an error for malformed operands when reached, rather than returning false. A present null actual value raises a comparison error. Missing paths keep the existing check behaviour.
Timestamps#
before and after accept YYYY-MM-DDTHH:mm:ss followed by Z or an offset such as +02:00, with an optional fraction of one to nine digits before the timezone. 2026-09-13T12:30:45.123456789Z and 2026-09-13T12:30:45+02:00 are both valid.
- The time and timezone are required.
- Fractions are truncated to milliseconds, not rounded.
- Offsets cannot exceed 14 hours.
- The date, and the instant in UTC, must fall between the years 0001 and 9999.
- Dates use the Gregorian calendar. Invalid calendar dates, leap seconds, hour 24 and year 0000 are rejected.
Date only strings such as 2026-01-01, timestamps without a timezone, epoch numbers and other date formats are not accepted. Invalid inputs make direct before and after comparisons return false in SDKs, but a surrounding not may invert that result. An invalid timestamp fails a reached date comparison in a test check.
Native date values#
These SDKs also accept native values representing an instant. Native inputs follow the same year range and millisecond truncation as strings. When the type retains a local year or timezone offset, the local year must also be within 0001 to 9999 and the offset cannot exceed 14 hours.
| SDK | Native input |
|---|---|
| JavaScript | Date |
| Go | time.Time |
| Python | Aware datetime with a UTC offset |
| Java | Instant, java.util.Date, OffsetDateTime, ZonedDateTime |
| Ruby | Time, DateTime |
| PHP | DateTimeInterface |
| Swift | None, use timestamp strings |
| .NET | DateTimeOffset, DateTime with Kind Utc or Local |
| Dart | DateTime |
| Elixir | DateTime |
| Rust | AttributeValue::Date |
Values without a time or timezone are rejected: Ruby Date, Python date and naive datetime, Java LocalDate and LocalDateTime, .NET DateOnly and DateTime with Kind Unspecified, and Elixir Date and NaiveDateTime.
Go's zero time.Time{} is the valid instant 0001-01-01T00:00:00Z, not an unknown date. It matches before most modern timestamps. Omit the context key or use a nil pointer when the date is unknown.
Semantic versions#
A version is ASCII digits and letters with no whitespace. It has one to four numeric parts, and missing parts count as zero, so 1, 1.0 and 1.0.0 are equal. A prerelease or build suffix needs at least three parts. Leading v, ^, ~, <, > and = are accepted but do not make a range. The operator decides the comparison.
Numeric parts compare exactly, even beyond a language's integer limit. A part after the first can be x, X or * to match anything, but later parts still count: 1.x.3 is less than 1.9.4.
Prerelease identifiers are separated by dots. One made only of digits is numeric, and numeric identifiers sort before text. Text compares case sensitively. When the shared identifiers are equal, the shorter prerelease sorts first. In a prerelease, x is ordinary text. Build metadata after + never affects the result.
A malformed version is an evaluation error, even inside not or or.
Regular expressions#
Projects support the flags g, i, m and s. g does not make evaluation stateful: every match starts from the beginning.
For patterns that behave the same in every SDK, use character classes, anchors, capturing groups, alternation, escaped literals and quantifiers. Linting rejects lookaround, backreferences, named or non capturing groups, atomic groups, inline flags and possessive quantifiers in attribute and variable conditions. Test checks validate their own patterns. The JavaScript SDK compiles patterns with RegExp, and does not run these checks when you pass it conditions directly.
Most SDKs match by backtracking, and context values such as an email or a user agent often come from your users. A group that repeats and can split the same text in more than one way, such as (a+)+, (\w+\s?)* or (a|aa)+, takes exponential time on a short input that almost matches. Lint rejects these groups. Make each repetition start or end with a character the rest cannot match, as in ([a-z0-9-]+\.)+[a-z]{2,}.
Engines still differ in places:
- Without
i, native SDKs treat\d,\D,\wand\Was ASCII classes, like JavaScript.\skeeps its usual whitespace meaning. - With
i, case folding outside ASCII depends on the engine, even for[A-Za-z]. - Prefer
[0-9]and[A-Za-z0-9_]when the intent matters, and test important patterns in each SDK you use, with and withouti.


