Core Concepts
A short tour of the primitives you'll use every day: flags, rules, contexts, environments, segments, and evaluation reasons.
Flags
A flag is a named, typed value the SDK resolves at runtime. Every flag has:
- A key: the string identifier you pass to
evaluate(). - A type:
boolean,string,number, orjson. - A default value: served when the flag is disabled, or when no rule matches.
- An enabled switch, the kill switch. When off, every evaluation returns the default.
- An ordered list of targeting rules.
Rules
All targeting lives in rules. A rule has an ordered list of conditions (all of which must match), a serve value, and an optional rollout percentage.
How evaluation runs
The engine walks the rules in order and takes the first decisive one:
- If the flag is disabled, return the flag's default with reason
flag_disabled. Done. - For each rule in order:
- If not every condition matches the context, skip to the next rule.
- If the rule has no rollout, serve its value with reason
rule_match. Done. - If the rule has a rollout, compute the user's bucket. Inside the percentage, serve its value with reason
rollout. Done. - Outside the percentage, fall through to the next rule.
- If no rule was decisive, return the flag's default with reason
default.
That fall-through is the subtle part, and it is useful: a user who misses a 10% rule is still eligible for every rule beneath it. Order your rules accordingly.
Evaluation context
The context is a flat bag of attributes you pass at evaluation time. Rules read from it. Values may be strings, numbers, or booleans, nested objects and arrays are not part of the attribute model.
userId: the bucketing key for percentage rollouts.email,plan,country: common targeting dimensions.- Anything else you want, custom attributes are first-class.
sha256("{userId}:{flagId}"), first 8 hex characters
read as an integer, mod 100. Same user plus same flag always lands in the
same bucket, on the server and in every SDK, so users never flip between
variants on refresh. If userId is absent the engine falls back
to email, and then to the empty string, which buckets every
anonymous user identically.
Segments
A segment is a named, reusable audience you can reference
from any rule with the segment_match operator, instead of
copying the same conditions into a dozen flags. Membership comes from three
places, and exclusion always wins.
- Included keys: always in.
- Excluded keys: always out, overriding both of the others.
- Rules: a user matches if any rule matches (and a rule matches when all of its conditions do).
See the Segments guide for worked examples.
Environments
Each project has independent environments (typically
development, staging, and production),
each with their own API keys and flag configurations. An SDK key is bound to
exactly one environment, which is how the SDK knows which flags to serve
without you naming the environment anywhere in code.
Evaluation reasons
Every result includes a reason so you can explain
why a flag returned what it did:
rule_match: a rule's conditions matched and it had no rollout.rollout: a rule matched and the user fell inside its rollout percentage.flag_disabled: the kill switch is off; the flag's default was returned.default: no rule was decisive; the flag's default was returned.flag_not_found: no flag exists with that key; your default was returned.
flag_not_found comes from. The REST API
answers an unknown key with HTTP 404, not a result body. The
SDKs turn that into a flag_not_found result carrying the default
you passed, so a typo in a flag key degrades to your fallback instead of
throwing.
Result shape
interface EvaluationResult {
flagKey: string;
value: boolean | string | number | Record<string, unknown>;
reason: 'default' | 'rule_match' | 'rollout' | 'flag_disabled' | 'flag_not_found';
/** UUID of the rule that decided this result — only on rule_match and rollout. */
ruleId?: string;
} Caching & freshness
The SDK keeps a local in-memory cache so synchronous get() calls
return without a network round trip. The cache is populated by
evaluate(), evaluateAll(), and the streaming
transport. Flag changes propagate over SSE within seconds; when streaming is
unavailable the client polls instead (30s by default).
That cache is also the SDK's failure story: it is served as last-known-good when a request fails, so an outage never silently reverts your users to compile-time defaults. See Resilience for the exact behavior per failure class.