Flag evaluation, version 1
This is the language-neutral definition of how flag evaluates a flag for a context. The Rust crate
crates/flag-eval is the reference implementation; every SDK implements this document and must pass the
conformance vectors in spec/vectors/ exactly:
evaluation.json:bucketing(salt, key, unit → bucket) andsuites, each a snapshot plus cases (flag key, context JSON → expected result).redaction.json: private-attribute redaction.envelope.json: opening an encrypted server snapshot (seesdk-protocol.md).
All JSON is UTF-8. “String” means a JSON string; “number” a JSON number, compared as an IEEE 754 double.
Snapshot
{
"envId": "env_...",
"version": 42,
"hashedTargets": false,
"flags": { "<flag key>": Flag },
"segments": { "<segment key>": Segment }
}
Unknown fields anywhere must be ignored, so the server can add fields without breaking older SDKs.
Flag
| Field | Type | Meaning |
|---|---|---|
key |
string | The flag key. |
version |
integer | Flag version, reported in events. |
on |
boolean | Targeting on. Default false. |
variations |
array of JSON values | The possible values. Results name them by index. |
offVariation |
integer, optional | Served when the flag is off or a prerequisite fails. |
fallthrough |
Serve | Served when no target or rule matches. |
targets |
array of Target | Individual targets, checked in order. |
rules |
array of Rule | Checked in order after targets. |
prerequisites |
array of {key, variation} |
Flags that must be on and serve that variation. |
salt |
string | Bucketing salt. |
clientSide |
boolean | Visible to client-side keys. |
trackEvents |
boolean | SDKs send a full feature event per evaluation. |
Serve is {"variation": i} or {"rollout": Rollout}. If both are present the rollout wins. If neither is,
the flag is malformed.
Target: {"contextKind": "user", "variation": i, "values": ["key", ...]}. contextKind defaults to "user".
Rule: {"id": "...", "clauses": [Clause, ...], ...Serve, "trackEvents": false}. A rule matches when every clause
matches; a rule without clauses matches every context. The Serve fields (variation, rollout) sit directly in
the rule object.
Rollout:
{
"variations": [{"variation": 0, "weight": 50000}, {"variation": 1, "weight": 50000}],
"bucketBy": "key",
"contextKind": "user",
"seed": "optional; replaces the flag's salt",
"experiment": {
"id": "exp_...",
"defaultVariation": 0,
"layer": {"key": "...", "salt": "...", "start": 0, "end": 50000},
"holdout": {"key": "...", "salt": "...", "start": 0, "end": 5000}
}
}
Weights are integers in units of 1/100,000. bucketBy defaults to "key", contextKind to "user".
Segment
| Field | Type | Meaning |
|---|---|---|
key, version, salt |
As for flags. | |
included, excluded |
array of string | User-kind keys always in / always out. |
includedContexts, excludedContexts |
array of {contextKind, values} |
The same for any kind. |
rules |
array of SegmentRule | {"id", "clauses", "weight"?, "bucketBy": "key", "rolloutContextKind": "user"} |
Contexts
A context is a JSON object.
- Single kind:
{"kind": "user", "key": "u1", "<attribute>": value, ..., "_meta": {...}}.kinddefaults to"user". - Multi kind:
{"kind": "multi", "<kind>": {single context without kind}, ...}, at least one kind.
A context is invalid, and evaluation returns the error INVALID_CONTEXT, if it is not an object, kind is not a
string, a kind name is empty, is "kind" or "multi", or has characters other than ASCII letters, digits, .,
_ and -, or any kind lacks a non-empty string key. The attributes of a kind are its fields except kind and
_meta; key is an attribute.
Attribute references. A name that does not start with / names a top-level attribute literally (so a/b is
the attribute called a/b). A name that starts with / is a path: split the rest on /, replace ~1 with /
and then ~0 with ~ in each part, take the first part as a top-level attribute and each further part as a
field of a JSON object (anything else: missing). A JSON null value counts as missing.
Private attributes
Before a context leaves an SDK in an event, the SDK removes private attributes: the names configured globally
on the SDK, then those in the context’s _meta.privateAttributes, each a reference as above. key and kind
are never removed. The context’s _meta is replaced by {"redactedAttributes": [references actually removed, in that order]}, or dropped if nothing was removed. In a multi context each kind is redacted on its own.
Evaluation
evaluate(snapshot, flagKey, context) returns {value, variation, reason, experimentId?}. value is the
variation’s value, or null with variation: null when the caller’s default must be served.
- If the snapshot has no flag
flagKey: errorFLAG_NOT_FOUND. - If the context is invalid: error
INVALID_CONTEXT. - Evaluate the flag:
- If the flag is on the stack of flags being evaluated (a prerequisite cycle): error
MALFORMED_FLAG. - If
onis false: serveoffVariationwith reasonOFF(ornulland no variation, reasonOFF). - For each prerequisite in order: if the snapshot lacks the flag, or evaluating it (recursively, with this
context) does not give the required variation, or that flag is not
on, serve the off variation with reasonPREREQUISITE_FAILEDandprerequisiteKey. If the prerequisite’s evaluation is aMALFORMED_FLAGerror, return that error. - For each target in order: if the context has the target’s kind and that kind’s key is in
values, serve the target’s variation, reasonTARGET_MATCH. - For each rule in order (index
i): if it matches, serve it, reasonRULE_MATCHwithruleIndex: iandruleId. - Serve
fallthrough, reasonFALLTHROUGH.
- If the flag is on the stack of flags being evaluated (a prerequisite cycle): error
- Serving a variation index that is not in
variations(includingoffVariation): errorMALFORMED_FLAG.
An error result is {"value": null, "variation": null, "reason": {"kind": "ERROR", "errorKind": "..."}}.
SDKs add WRONG_TYPE when a typed accessor (boolean, string, number, object) gets a value of another JSON type;
they return the caller’s default for every error.
Clauses
{"contextKind": "user", "attribute": "email", "op": "endsWith", "values": ["@acme.com"], "negate": false}
segmentMatch: matches if the context is in any segment listed invalues(non-strings and unknown segments are skipped).negateinverts the result.attributeandcontextKindare ignored.- An operator not listed below never matches, whatever
negatesays. - Attribute
kind: the actual value is the array of the context’s kind names (anycontextKind). - Otherwise, if the context lacks
contextKindor the attribute is missing, the clause does not match, whatevernegatesays. - If the actual value is an array, the clause matches when any element matches; otherwise when the value
matches. A value matches when the operator holds for it and any of
values. Thennegateinverts the result.
op |
Holds when |
|---|---|
in |
JSON equality; numbers compare by value (30 equals 30.0); arrays and objects deeply. |
startsWith, endsWith, contains |
Both are strings and the string test holds (case-sensitive, by code point). |
matches |
The actual value is a string and the clause value, a regular expression, is found anywhere in it. |
lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual |
Both are numbers (no string coercion). |
before, after |
Both are dates and actual is strictly earlier / later. |
semVerEqual, semVerLessThan, semVerGreaterThan |
Both are strings that parse as versions, compared by precedence. |
segmentMatch |
See above. |
Regular expressions use the syntax common to RE2, ECMAScript and Python (no look-around, no backreferences)
with RE2’s meaning: the shorthand classes are ASCII (\d is [0-9], \w is [0-9A-Za-z_], \s is
[\t\n\f\r ], \b and \B are ASCII word boundaries, \D \W \S their negations, inside and outside
character classes), . matches one Unicode code point other than \n, $ matches only at the very end, and
escaped punctuation (\-, \:) is that character. Engines that differ
rewrite the pattern first (the Rust implementation, ECMAScript’s \s, Python’s $ and \s). A value that is not
a string or not a valid pattern never matches.
Dates are numbers (Unix milliseconds, floored) or strings in exactly the form
YYYY-MM-DDTHH:MM:SS[.fraction](Z|+HH:MM|-HH:MM), where T and Z may be lowercase. Fractions of any length are
truncated to milliseconds. Field ranges are checked (days per month with leap years, hours 0-23, minutes and
seconds 0-59, offsets up to 23:59). Anything else is not a date.
Versions follow Semantic Versioning 2.0.0, except that the minor and patch numbers may be left out (2 is
2.0.0, 2.1 is 2.1.0). No leading v, no leading zeros in numbers or numeric pre-release identifiers. Build
metadata (+...) must be well-formed and is ignored. Precedence: major, minor, patch numerically; a version
with a pre-release is lower than the same version without; pre-release identifiers compare left to right,
numeric ones numerically and below alphanumeric ones, alphanumeric ones by ASCII order; a shorter list that is a
prefix of a longer one is lower.
Segments
A context is in a segment when, in order:
- its user-kind key is in
included, or for someincludedContextsentry the context’s key of that kind is invalues: in; - its user-kind key is in
excluded, or the same forexcludedContexts: out; - some rule’s clauses all match and, if the rule has a
weight, the bucket of the context’srolloutContextKind/bucketByunit under (segmentsalt, segmentkey) is belowweight: in; - otherwise out.
Segments may test other segments (segmentMatch); a segment already being evaluated does not match.
Hashed targets
When the snapshot has hashedTargets: true (client-safe snapshots), every key in target values, segment
included/excluded and segment context values is the lowercase hex SHA-256 of the UTF-8 key. Compare the
hash of the context’s key instead of the key. Clause values are never hashed.
Bucketing
bucket(salt, key, unit) = floor(H * 100000 / 2^64)
H = the first 8 bytes of SHA-256(UTF-8 of salt + "." + key + "." + unit), as a big-endian unsigned 64-bit integer
Compute it with exact integer arithmetic (128-bit, big integers or mulhi): doubles lose bits.
The unit is the context’s attribute bucketBy of kind contextKind: a string as is, or a number with no
fractional part and magnitude at most 2^53 written in decimal without exponent or fraction (12345.0 is
"12345"). Any other value, a missing attribute or a missing kind leaves no unit.
Rollouts (no experiment): with salt = the rollout’s seed if present, else the flag’s salt, take
b = bucket(salt, flagKey, unit), or 0 when there is no unit. Walk variations adding up weights; serve the first
whose running sum exceeds b. If none does (weights short of 100,000), serve the last one. An empty list is
MALFORMED_FLAG.
Experiments (the rollout has experiment). The context is in the experiment only if all hold, else it gets
experiment.defaultVariation and is not in the experiment:
- it has a unit;
- if
holdoutis set:bucket(holdout.salt, holdout.key, unit)is not in[start, end); - if
layeris set:bucket(layer.salt, layer.key, unit)is in[start, end); - walking
variationsas above withb = bucket(salt, flagKey, unit), some running sum exceedsb(weights may add up to less than 100,000: the rest is outside the traffic allocation).
In the experiment, the reason gets "inExperiment": true and the result "experimentId". SDKs send an exposure
event for such results.
Reasons
{"kind": "OFF"}
{"kind": "TARGET_MATCH"}
{"kind": "RULE_MATCH", "ruleIndex": 0, "ruleId": "r1", "inExperiment": true}
{"kind": "FALLTHROUGH", "inExperiment": true}
{"kind": "PREREQUISITE_FAILED", "prerequisiteKey": "other-flag"}
{"kind": "ERROR", "errorKind": "FLAG_NOT_FOUND | MALFORMED_FLAG | INVALID_CONTEXT | WRONG_TYPE"}
inExperiment is present only when true. OpenFeature providers map them to resolution reasons: OFF →
DISABLED; TARGET_MATCH and RULE_MATCH → TARGETING_MATCH; FALLTHROUGH → SPLIT when the fallthrough is a
rollout, else DEFAULT; PREREQUISITE_FAILED → PREREQUISITE_FAILED; ERROR → ERROR with the OpenFeature
error code (FLAG_NOT_FOUND, PARSE_ERROR for malformed flags, INVALID_CONTEXT or TARGETING_KEY_MISSING,
TYPE_MISMATCH). The OpenFeature variant is the variation index as a decimal string.
OpenFeature contexts
An OpenFeature evaluation context maps to a flag context like this: targetingKey becomes key (unless key is
set); an attribute kind sets the kind; with kind: "multi", each object-valued attribute is one kind’s context,
whose targetingKey likewise becomes its key, and top-level attributes that aren’t objects (including
targetingKey) are dropped. Everything else is an attribute. A missing key maps to the OpenFeature error
TARGETING_KEY_MISSING, any other invalid context to INVALID_CONTEXT.
This page as Markdown: /docs/evaluation.md · All docs for agents: /llms.txt