Condition operators - Featurevisor
Featurevisor

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:

segments/netherlands.yml
description: The Netherlands
conditions:
- attribute: country
operator: equals
value: nl

The same operators work in three places:

  • Segment conditions, and inline conditions in force entries and overrides, compare an attribute. A dotted path such as account.seats reads a property of an object attribute.
  • Variable conditions compare a variable, optionally of a feature, with nested values read through path.
  • Test checks compare a result with a value, or with another result through compareTo.

This requirement uses includes on a global variable's array:

features/pricing.yml
# ...
conditions:
- variable: checkoutSettings
path: paymentMethods
operator: includes
value: ideal

To 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

segments/netherlands.yml
description: The Netherlands
conditions:
- attribute: country
operator: equals
value: nl

Matches a context whose country is exactly nl. The comparison is case sensitive, so NL does not match.

notEquals

segments/outsideNetherlands.yml
description: Everyone outside the Netherlands
conditions:
- attribute: country
operator: notEquals
value: nl

Matches 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

segments/teamAccounts.yml
description: Accounts with more than 10 seats
conditions:
- attribute: account.seats
operator: greaterThan
value: 10

An account with 11 seats matches, and one with 10 does not.

greaterThanOrEquals

segments/largeAccounts.yml
description: Accounts with 50+ seats
conditions:
- attribute: account.seats
operator: greaterThanOrEquals
value: 50

An account with exactly 50 seats matches.

lessThan

segments/smallAccounts.yml
description: Accounts with fewer than 5 seats
conditions:
- attribute: account.seats
operator: lessThan
value: 5

An account with 4 seats matches, and one with 5 does not.

lessThanOrEquals

segments/soloAccounts.yml
description: Accounts with one seat at most
conditions:
- attribute: account.seats
operator: lessThanOrEquals
value: 1

An 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

segments/dutchRegionLocale.yml
description: Locales for the Netherlands
conditions:
- attribute: account.locale
operator: contains
value: NL

nl-NL and fy-NL match, and nl-BE does not.

notContains

segments/nonBritishLocale.yml
description: Locales outside Britain
conditions:
- attribute: account.locale
operator: notContains
value: GB

en-US matches, and en-GB does not.

startsWith

segments/dutchLocale.yml
description: Dutch speaking accounts
conditions:
- attribute: account.locale
operator: startsWith
value: nl-

nl-NL and nl-BE match, and fy-NL does not.

endsWith

segments/belgianLocale.yml
description: Locales for Belgium
conditions:
- attribute: account.locale
operator: endsWith
value: -BE

nl-BE and fr-BE match, and nl-NL does not.

Membership

in and notIn check a value against a list.

in

segments/payingAccounts.yml
description: Paid and enterprise accounts
conditions:
- attribute: plan
operator: in
value:
- paid
- enterprise

Matches a context whose plan is any value in the list.

notIn

segments/nonEnterpriseAccounts.yml
description: Accounts on any plan but enterprise
conditions:
- attribute: plan
operator: notIn
value:
- enterprise

Matches 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.

OperatorComparison
includesContains one value
notIncludesDoes not contain one value
includesAnyContains any candidate
notIncludesAnyContains no candidate

includes

segments/editors.yml
description: Accounts that can edit
conditions:
- attribute: permissions
operator: includes
value: write

Matches [read, write], and not [read].

notIncludes

segments/nonAdmins.yml
description: Accounts without admin rights
conditions:
- attribute: permissions
operator: notIncludes
value: admin

Matches [read, write], and not [read, admin].

includesAny

segments/editorsOrAdmins.yml
description: Accounts that can edit or administer
conditions:
- attribute: permissions
operator: includesAny
value: [write, admin]

Matches [read, write] and [admin], but not [read].

notIncludesAny

segments/readOnlyAccounts.yml
description: Accounts without editing rights
conditions:
- 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

segments/beneluxLocale.yml
description: Dutch or Belgian locales
conditions:
- attribute: account.locale
operator: matches
value: ^(nl|be)-
regexFlags: i

With the i flag, nl-NL and NL-nl both match.

notMatches

segments/nonEnglishLocale.yml
description: Locales in any language but English
conditions:
- 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

segments/earlyAccounts.yml
description: Accounts created before 2026
conditions:
- attribute: signupDate
operator: before
value: 2026-01-01T00:00:00Z

An account created on 2025-12-31T23:59:59Z matches.

after

segments/newAccounts.yml
description: Accounts created this year
conditions:
- attribute: signupDate
operator: after
value: 2026-01-01T00:00:00Z

An 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

segments/currentRelease.yml
description: App version 5.5.0
conditions:
- attribute: version
operator: semverEquals
value: 5.5.0

5.5.0 matches, and so does 5.5, since missing parts count as zero.

semverNotEquals

segments/notBrokenRelease.yml
description: Every version but 5.4.1
conditions:
- attribute: version
operator: semverNotEquals
value: 5.4.1

Leaves out one release, such as one with a known bug.

semverGreaterThan

segments/afterCurrentRelease.yml
description: App versions after 5.5.0
conditions:
- attribute: version
operator: semverGreaterThan
value: 5.5.0

5.10.0 matches, because versions compare by number.

semverGreaterThanOrEquals

segments/supportedVersion.yml
description: App version 5.5.0 or later
conditions:
- attribute: version
operator: semverGreaterThanOrEquals
value: 5.5.0

A prerelease such as 5.5.0-beta.1 sorts before 5.5.0, so it does not match.

semverLessThan

segments/legacyVersions.yml
description: App versions before 5.0.0
conditions:
- attribute: version
operator: semverLessThan
value: 5.0.0

4.9.9 matches, and 5.0.0 does not.

semverLessThanOrEquals

segments/oldVersions.yml
description: App version 5.4.0 or earlier
conditions:
- attribute: version
operator: semverLessThanOrEquals
value: 5.4.0

5.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

segments/signedIn.yml
description: Signed in users
conditions:
- attribute: userId
operator: exists

Matches any context with a userId, whatever its value.

notExists

segments/anonymousVisitors.yml
description: Visitors who are not signed in
conditions:
- attribute: userId
operator: notExists

Matches a context without a userId.

isNull

segments/noAccount.yml
description: Users whose account is null
conditions:
- attribute: account
operator: isNull

Matches when the application passes account: null, and not when it leaves account out.

notNull

segments/withAccount.yml
description: Users with an account
conditions:
- attribute: account
operator: notNull

Matches any account that is present and not null.

empty

segments/noPermissions.yml
description: Accounts without permissions
conditions:
- attribute: permissions
operator: empty

Matches permissions: [].

notEmpty

segments/withPermissions.yml
description: Accounts with any permission
conditions:
- attribute: permissions
operator: notEmpty

Matches [read], and not [].

Which values match

The six operators side by side, for each kind of value:

ValueexistsisNullnotNullemptynotEmpty
MissingNoNoNoNoNo
nullYesYesNoNoNo
"", [], {}YesNoYesYesNo
" ", [null]YesNoYesNoYes
A number or trueYesNoYesNoNo

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:

  • notEquals returns true, as described under Equality.
  • notExists returns true.
  • not around another operator turns its false into true. So not around empty matches a missing value, while notEmpty does not. Likewise, notNull requires a value to be present, while not around isNull does 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:

  • in and notIn in conditions need a string attribute, including nested paths, and a list of strings.
  • in and notIn variable condition lists may contain strings, numbers and null, but never booleans. Compare booleans with equals or notEquals.
  • includesAny and notIncludesAny conditions need a string array attribute or path. Each candidate must be a string matching its declared item constraints, including enums. For permissions, this means only read, write or admin.
  • In conditions, these two array list operators accept broader primitive lists: strings, numbers, booleans or null, 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 greaterThan on a string or empty on a number.
  • Date attributes reject equality, string and emptiness operators. Their enum and const values 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.

SDKNative input
JavaScriptDate
Gotime.Time
PythonAware datetime with a UTC offset
JavaInstant, java.util.Date, OffsetDateTime, ZonedDateTime
RubyTime, DateTime
PHPDateTimeInterface
SwiftNone, use timestamp strings
.NETDateTimeOffset, DateTime with Kind Utc or Local
DartDateTime
ElixirDateTime
RustAttributeValue::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.

ComparisonResult
1.2.3.4 with 1.2.3.5Less than
1.0.0-0 with 1.0.0Less than
1.0.0-1a with 1.0.0-1bLess than
1.0.0-10 with 1.0.0-1aLess than
1.0.0-alpha with 1.0.0-alpha.0Less than
1.0.0+build.1 with 1.0.0+build.2Equal

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, \w and \W as ASCII classes, like JavaScript. \s keeps 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 without i.
Previous
Conditions