The targeting rule is a predicate
A targeting rule is a boolean predicate evaluated against a user context at flag evaluation time. The user context is a map of attributes your application provides at runtime:
const context = {
userId: 'usr_8f3k2',
email: 'ada@example.com',
plan: 'pro',
country: 'FR',
accountAgeDays: 142,
betaOptIn: true,
};
const enabled = await signal.isEnabled('new-dashboard', context);Signal evaluates the rules you have defined in the dashboard against this context. If the rules match, the flag resolves to its enabled state. If not, it falls back to the default.
Rule operators
Signal supports eight rule operators:
| Operator | Example |
|---|---|
equals |
plan == "enterprise" |
not_equals |
plan != "free" |
contains |
email contains "@corp.example.com" |
starts_with |
userId starts_with "internal_" |
in |
country in ["FR", "DE", "ES"] |
greater_than |
accountAgeDays > 90 |
less_than |
accountAgeDays < 7 |
exists |
betaOptIn exists |
Rules combine with AND within a condition group, and OR across groups. This maps to: any user who matches at least one group, where each group requires all its conditions to be true.
Reusable segments
Targeting rules that are used across multiple flags should be extracted into segments. A segment is a named, reusable predicate:
Segment: "Enterprise accounts"
Rules: plan == "enterprise" AND accountAgeDays > 90
Attach this segment to any flag. When your enterprise definition changes — say, you add a new tier — update the segment once. Every flag that references it picks up the change immediately.
Without segments, updating the enterprise definition means hunting down every flag that hardcodes plan == "enterprise" and updating each one. One missed flag means inconsistent behaviour.
Ordering matters
When a flag has multiple targeting rules, Signal evaluates them in order and returns the first match. This has practical implications for how you structure rules.
A common pattern: most restrictive rule first.
Rule 1: userId in ["internal_001", "internal_002"] → enabled: true (internal team)
Rule 2: plan == "enterprise" → enabled: true (enterprise users)
Rule 3: betaOptIn == true AND plan != "free" → rollout: 10% (opted-in paying users)
Default: enabled: false
If you put the rollout rule before the explicit enable rules, your internal team members who happen to be paying users might get caught in the 10% rollout rather than guaranteed access.
Testing your context
The most common targeting bug is a mismatch between the attributes you pass at evaluation time and the attributes you used to write the rules.
If your rule says plan == "enterprise" but your application passes plan: "Enterprise" (capital E), the rule will not match. Signal is case-sensitive.
To debug, log the full evaluation context in development:
const signal = getServerInstance();
// In development, enable evaluation logging
const result = await signal.evaluate('new-dashboard', context);
console.log(result.context, result.matched_rule, result.value);A layered targeting strategy
For most teams, a layered strategy works well:
Layer 1 — Internal team. Enable for your engineering and product team by email domain or a hardcoded user ID list. This is your canary for every flag.
Layer 2 — Beta users. An explicit opt-in attribute (betaOptIn: true) lets users self-select. This gives you a stable cohort of users who expect rough edges and give good feedback.
Layer 3 — Segment-based rollout. Target a specific tier, region, or cohort with 100% exposure before any broader rollout.
Layer 4 — Percentage rollout. Once you have signal from the layers above, open the rollout to a percentage of the remaining audience.
Default — Off. The default state should always be the safe, existing behaviour. Never make the default the new, untested code path.
This layered approach gives you multiple observation points before broad exposure, and a clean escalation path when something goes wrong.