Targeting Rules
FlagRemoteState.targetingRules is a TargetingRule[] — a list evaluated
first-match-wins, not “all rules must match.” This page walks through
each rule type and how the list itself is evaluated.
Rule types
Section titled “Rule types”type TargetingRule = | { type: "attributeEquals"; attribute: string; value: string | number | boolean } | { type: "attributeIn"; attribute: string; values: (string | number)[] } | { type: "percentageRollout"; percentage: number; bucketingSeed?: string } | { type: "semverRange"; attribute: string; range: string } | { type: "dateRange"; startAt?: string; endAt?: string } | { type: "and"; rules: TargetingRule[] } | { type: "or"; rules: TargetingRule[] };attributeEquals
Section titled “attributeEquals”Matches context.attributes[attribute] === value:
{ type: 'attributeEquals', attribute: 'plan', value: 'enterprise' }attributeIn
Section titled “attributeIn”Matches when the attribute is a string or number present in values:
{ type: 'attributeIn', attribute: 'countryCode', values: ['US', 'CA', 'MX'] }percentageRollout
Section titled “percentageRollout”Bucketed via the same bucketing key + salting
as everything else — bucketingSeed, if given, changes the salt so this
rule’s rollout doesn’t correlate with other flags’ or other rules’
rollouts:
{ type: 'percentageRollout', percentage: 25, bucketingSeed: 'checkout-v2-ring-1' }If no bucketing key resolves for the context (see the resolution chain), this rule never matches.
semverRange
Section titled “semverRange”Matches when context.attributes[attribute] is a string satisfying a
node-semver range:
{ type: 'semverRange', attribute: 'appVersion', range: '>=4.2.0' }dateRange
Section titled “dateRange”Matches when Date.now() falls within [startAt, endAt] (either bound is
optional — an open start or open end):
{ type: 'dateRange', startAt: '2026-09-01T00:00:00Z', endAt: '2026-09-15T00:00:00Z' }Composing with and / or
Section titled “Composing with and / or”and requires every nested rule to match; or requires at least one.
They nest arbitrarily:
{ type: 'and', rules: [ { type: 'attributeEquals', attribute: 'plan', value: 'enterprise' }, { type: 'or', rules: [ { type: 'attributeIn', attribute: 'region', values: ['us-east', 'us-west'] }, { type: 'attributeEquals', attribute: 'betaOptIn', value: true }, ], }, ],}Reads as: enterprise plan and (US region or opted into beta).
First-match-wins over the list
Section titled “First-match-wins over the list”targetingRules on FlagRemoteState is a flat list, not a single
composite rule — put your and/or composition inside one list entry when
you need multiple conditions on one match, and use separate list entries
when you want independent fallback rules tried in order:
const targetingRules: TargetingRule[] = [ { type: 'attributeEquals', attribute: 'internalTester', value: true }, { type: 'attributeEquals', attribute: 'plan', value: 'enterprise' }, { type: 'percentageRollout', percentage: 10 },];The engine walks this list top to bottom and stops at the first rule that
matches (evaluateRules in @useoptimus/core, which evaluate() calls
internally — see evaluateRules).
An internal tester always matches the first rule regardless of plan; a
non-tester enterprise user matches the second; everyone else falls through
to the 10% rollout. A matched rule produces reason: 'targetingMatch' (or
'rollout' for a percentageRollout match) on the EvaluatedFlag — note
that EvaluatedFlag.ruleMatched is typed but not currently populated by
evaluate(); use evaluateRules() directly (it returns { matched, rule }) if you need to know which rule fired.
If no rule matches, evaluation falls through to rolloutPercentage /
defaultValue handling as usual — see Flag
Taxonomy.