How flags are evaluated
How a client library turns a flag definition and an evaluation context into a value: true or false for a boolean flag, or one of a multivariate flag's variations. Every Switchyard library must follow this exactly and pass evaluation-vectors.json, so the same user gets the same answer in Java, JavaScript or anything else.
Inputs
The SDK endpoint (GET /sdk/v1/flags) sends every active flag of one environment, plus the project's segments:
{
"environment": "production",
"version": 42,
"flags": {
"new-checkout": {
"enabled": true,
"version": 7,
"rules": [
{
"clauses": [{ "attribute": "email", "operator": "ends_with", "values": ["@acme.com"] }],
"serve": { "value": true }
},
{
"clauses": [{ "attribute": "segment", "operator": "in_segment", "values": ["beta-testers"] }],
"serve": { "rollout": { "percentage": 50 } }
}
],
"fallthrough": { "rollout": { "percentage": 10 } }
},
"checkout-button": {
"enabled": true,
"version": 9,
"type": "string",
"variations": ["green", "blue", "orange"],
"offVariation": 0,
"prerequisites": [{ "flag": "new-checkout", "value": true }],
"rules": [],
"fallthrough": { "rollout": { "variations": [{ "variation": 1, "weight": 50 }, { "variation": 2, "weight": 50 }] } }
}
},
"segments": {
"beta-testers": {
"included": ["user-7", "user-9"],
"excluded": ["user-13"],
"rules": [{ "clauses": [{ "attribute": "plan", "operator": "in", "values": ["enterprise"] }] }]
}
}
}Flags
| Field | Meaning |
|---|---|
enabled |
The kill switch. |
type |
boolean (the default when absent), string, number or json. |
variations |
Multivariate flags only: the values the flag can serve, 2 to 20 of them, all of type (json allows any JSON value). |
offVariation |
Multivariate flags only: the index of the variation served when the flag is off or a prerequisite fails. |
prerequisites |
Other flags that must give a particular value first. May be absent: treat as []. |
rules, fallthrough |
May be absent (servers before targeting): treat as [] and { "value": true }. |
The off value is false for a boolean flag, and variations[offVariation] for a multivariate flag.
Segments
Reusable groups of contexts, shared by every flag in the project. included, excluded and rules may each be absent: treat as empty.
Evaluation context
Who or what the flag is being checked for:
key: a stable, unique identifier (user ID, account ID, device ID). Needed for percentage rollouts. May be absent.attributes: string → string, e.g.{"email": "a@acme.com", "country": "IN"}.
Algorithm
- Unknown flag (not in the payload): return the caller's default.
- Off (
enabledisfalse): return the off value. Prerequisites and rules are ignored; off is a kill switch. - Prerequisites, in order: evaluate the named flag for the same context (steps 2 to 5, recursively). If that flag isn't in the payload, or its result isn't equal to
value(JSON equality), return the off value. A chain more than 10 flags deep, or one that comes back to a flag already being evaluated, also fails: return the off value. - Rules, in order: the first rule whose clauses all match decides the result, via its
serve. A rule with no clauses never matches. - Fallthrough: if no rule matches,
fallthroughdecides.
Clauses
The clause's attribute value is context.key when attribute is "key", otherwise context.attributes[attribute]. Segment operators are the exception: they don't read an attribute (servers write "segment" there).
- If the context has no value for the attribute, the clause does not match, whatever the operator (so
not_indoesn't match a missing attribute either). - A clause matches when the attribute value satisfies the operator for any of
values, exceptnot_inandnot_in_segment, which need none to match. - An unknown operator never matches, so an older library ignores a rule using an operator added later rather than failing.
Text
Exact and case-sensitive. Store lowercase values and send lowercase attributes where case shouldn't matter, e.g. emails.
| Operator | Matches when the attribute value… |
|---|---|
in |
equals any of values |
not_in |
equals none of values |
starts_with |
starts with any of values |
ends_with |
ends with any of values |
contains |
contains any of values |
matches |
contains a match for any of values, as regular expressions |
matches searches anywhere in the value; anchor with ^ and $ to match all of it. Use only syntax that Java and JavaScript read the same way: literal characters, ., character classes like [a-z] and \d, *, +, ?, {m,n}, |, groups, ^ and $. A pattern that doesn't compile never matches.
Numbers
lt, lte, gt, gte: the attribute value is less than, at most, greater than, or at least any of values. Both are read as decimal numbers: an optional -, digits, and an optional fraction (42, -3, 2.50). An attribute value that isn't one never matches; a value in values that isn't one is skipped.
Versions
semver_eq, semver_lt, semver_gt: compare as Semantic Versioning 2.0.0 versions, so 1.10.0 is greater than 1.9.0 and 2.0.0-beta is less than 2.0.0. A missing minor or patch number counts as 0 (2 is 2.0.0); build metadata (+…) is ignored. A leading v is not allowed. Anything that isn't a version never matches (or is skipped, in values).
Prerelease precedence, as Semantic Versioning defines it: compare the dot-separated identifiers left to right; numeric identifiers compare as numbers and sort before alphanumeric ones; otherwise compare ASCII order; a longer list wins when all earlier identifiers are equal.
Dates
before, after: the attribute value is earlier or later than any of values. Both are either an RFC 3339 timestamp with a time zone (2026-11-01T09:30:00Z, 2026-11-01T15:00:00.250+05:30) or a whole number of milliseconds since 1970-01-01T00:00:00Z. Comparison is to the millisecond: digits beyond milliseconds are ignored. Anything else never matches (or is skipped, in values).
Segments
in_segment matches when the context is in any segment named in values; not_in_segment matches when it's in none of them. A segment the payload doesn't contain contains nobody.
A context is in a segment when:
- its key is in
included, or else - its key is not in
excluded, and any of the segment'srulesmatches (all its clauses match, as above).
Without a key, only step 2's rules can match. Segment rules can't use segment operators: inside a segment, in_segment and not_in_segment never match.
Serve
serve and fallthrough have exactly one of:
| Serve | Flag type | Result |
|---|---|---|
{ "value": true | false } |
boolean | that value |
{ "rollout": { "percentage": p } } |
boolean | true for p% of contexts: bucket < round(p × 100) |
{ "variation": i } |
multivariate | variations[i] |
{ "rollout": { "variations": [{ "variation": i, "weight": w }, …] } } |
multivariate | Walk the list adding round(w × 100) to a running total; the first entry where bucket < the total is served. If none is (weights adding up to less than 100), the last entry is served |
Percentages and weights are between 0 and 100 with at most two decimals; a multivariate rollout's weights add up to 100. A serve this library doesn't understand, or a variation index outside variations, gives the off value.
Buckets
bucket = murmur3_32(utf8(flagKey + ":" + contextKey)) mod 10000 // 0 … 9999, unsignedmurmur3_32is MurmurHash3 x86 32-bit with seed0, read as an unsigned 32-bit integer.- The flag key is part of the input, so a context's bucket differs between flags: being in one flag's 10% says nothing about another's.
- A context keeps its bucket for a flag forever. Raising a percentage only adds contexts; everyone already in stays in. Lowering it removes the most recently added first. In a multivariate rollout, moving weight between neighbouring variations only moves the contexts at the boundary.
- No context key: the bucket is
9999, the last one. So a boolean rollout includes a context without a key only at 100%, and a multivariate rollout serves it the last variation with any weight.
Changing the format
Additive changes (new operators, new fields, new serve kinds) are safe: older libraries ignore unknown fields, treat unknown operators as non-matching, and give the off value for a serve they don't understand. Anything else needs a new SDK endpoint version (/sdk/v2).