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:

json
{
  "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:

Algorithm

  1. Unknown flag (not in the payload): return the caller's default.
  2. Off (enabled is false): return the off value. Prerequisites and rules are ignored; off is a kill switch.
  3. 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.
  4. Rules, in order: the first rule whose clauses all match decides the result, via its serve. A rule with no clauses never matches.
  5. Fallthrough: if no rule matches, fallthrough decides.

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).

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:

  1. its key is in included, or else
  2. its key is not in excluded, and any of the segment's rules matches (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, unsigned

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).