Resolution
For a given (flag, environment, evaluation context) triple, exactly one variant is returned. This page is the reference for how that variant is chosen: the four-step algorithm, the catch-all behavior, and the testing rollout gate.
The per-block field reference lives in flag § [flag.environments.<env>] blocks. This page is the algorithm.
The four-step algorithm
For an evaluation against environment <env>, with the eval-time option include_testing (default false — see § The rollout workflow):
- Env rules. If
[flag.environments.<env>]exists AND declares arulesarray AND eithertesting = false(the default) OR the caller passedinclude_testing = true, walk the array in declared order. For each rule:- Evaluate the rule's audience (segment lookup OR inline predicate) against the evaluation context.
- If the audience matches, return the rule's
variant.rule_matched = "rule:<index>"(zero-based).
- Env default. If
[flag.environments.<env>]declaresvariant, return it.rule_matched = "default". _rules. Reached only when step 1 was skipped because<env>did not declare its ownrules. Walk[flag.environments._.rules]in declared order. First match → return itsvariantwithrule_matched = "rule:<index>"(indexed against_'s array)._default. Return[flag.environments._].variant.rule_matched = "default".
Short-circuits
Three short-circuits in the walk:
- Env declared
rules→ step 3 is skipped. The env's rules completely replace_'s rules; there is no layering. The skip is based on the declared manifest shape, not on whether the rules array actually fired for this caller — sotesting = true+ no opt-in still skips step 3. - Env declared
variant→ step 4 is skipped. The env's default completely replaces_'s default. testing = true+ noinclude_testing→ step 1 is skipped (the env's rules are invisible to this caller), but steps 2–4 proceed with their normal skip rules. Non-opted-in callers see the env's declaredvariantif any, else_'svariant. The env's testing rules never leak to non-opted-in callers, and_'s rules never apply to non-opted-in callers in atesting = trueenv.
Combinatorial summary
<env> block declares… | Resolution path |
|---|---|
| Nothing (or no block) | step 3 → step 4 |
variant only | step 2 (_ never consulted) |
rules only | step 1 → step 4 |
rules + variant | step 1 → step 2 (_ never consulted) |
testing = true + rules + caller passes include_testing | step 1 → step 2 / step 4 |
testing = true + rules + caller omits include_testing | step 2 if variant, else step 4 (rules invisible; step 3 stays skipped) |
The catch-all _
_ is the env block that applies when the requested environment did not supply a piece of the answer. It MUST exist on every flag, and it MUST declare a variant.
_ is reserved as an env name regardless of mode (typed or untyped). The linter does NOT require _ to appear in [namespace.environments] — the reserved name is structural.
The simplest possible flag declares only [flag.environments._] with a variant and no rules. Resolution then returns the same variant for every env.
Worked traces
Catch-all only
[flag.environments._]
variant = "off"
[[flag.environments._.rules]]
segment = "internal-employees"
variant = "on"
| Evaluation | Trace | Result |
|---|---|---|
staging env (no staging block), internal employee | step 1 skipped → step 2 skipped → step 3 _ rule matches → "on" | "on" |
staging env, external user | step 1 skipped → step 2 skipped → step 3 no match → step 4 _ default | "off" |
Production with rules only
Adding a production block with rules but no variant:
[flag.environments.production]
[[flag.environments.production.rules]]
segment = "checkout-redesign-rollout-10"
variant = "on"
| Evaluation | Trace | Result |
|---|---|---|
production, in rollout-10 | step 1 match → "on" | "on" |
production, internal employee NOT in rollout-10 | step 1 no match → step 2 skipped → step 3 skipped (env declared rules) → step 4 _ default | "off" |
The _ rule for internal-employees does NOT fire in production because production declared its own rules array. Internal employees who need "on" in production must have a matching rule duplicated into production's block. This is intentional: env rules are a complete override, not an additive layer.
Self-contained production
Adding a variant to production:
[flag.environments.production]
variant = "off"
[[flag.environments.production.rules]]
segment = "checkout-redesign-rollout-10"
variant = "on"
| Evaluation | Trace | Result |
|---|---|---|
production, in rollout-10 | step 1 match → "on" | "on" |
production, anyone else | step 1 no match → step 2 production default | "off" |
production is now fully self-contained; _ is never consulted.
Production kill-switch
Pin an env to a single variant — the kill-switch shape:
[flag.environments.production]
variant = "off"
production has no rules; step 1 is skipped, step 2 returns "off" immediately. _'s rules and default are never consulted in production.
The rollout workflow
A flag in a given env progresses through three observable states. Each state corresponds to a specific env-block shape; there is no state field — the shape is the state.
| State | Env-block shape | Behavior |
|---|---|---|
| Disabled | variant = "<off>", no rules (the kill-switch shape) | Step 1 empty (no rules), step 2 returns the off-variant. _ never consulted. |
| Testing | testing = true, rules declared, variant strongly recommended | Step 1 evaluates only for callers that pass include_testing = true; everyone else sees step 2 or step 4. |
| Enabled | rules declared (and optionally variant); testing absent or false | Step 1 evaluates for every caller. |
A typical rollout flips through the three states by editing only the env block, with no churn on rules or variants:
# Day 0 — disabled in production.
[flag.environments.production]
variant = "off"
# Day 1 — testing in production. Internal admins opt in via the SDK call.
# Everyone else still sees `off`. The `_` catch-all is never consulted for
# this env because the production block declares its own `rules` (step 3
# stays skipped).
[flag.environments.production]
testing = true
variant = "off"
[[flag.environments.production.rules]]
description = "Admin preview"
segment = "internal-admins"
variant = "on"
# Day 7 — testing pronounced safe; flip the gate off. Same rules, same
# variant. Now every prod request walks the rules.
[flag.environments.production]
variant = "off"
[[flag.environments.production.rules]]
description = "Admin preview"
segment = "internal-admins"
variant = "on"
The opt-in is per-eval-call, not a token scope or namespace setting: the same admin user evaluating the same flag with include_testing = false still sees off. This keeps the opt-in scoped to the explicit UI surface (typically an internal admin tool) and prevents accidental leakage through unrelated code paths.
Caller surfaces
Every SDK and the server's POST /evaluate{,/all} body accept the opt-in. Calls without it default to include_testing = false.
| Surface | Opt-in syntax |
|---|---|
| Rust SDK | Namespace::eval_with_options(flag, env, ctx, EvalOptions::new().with_include_testing(true)). Typed variants: eval_bool_with_options, eval_string_with_options, etc. |
| TypeScript SDK | client.eval(flag, env, ctx, { includeTesting: true }). Same includeTesting field on evalBool, evalString, evalI64, evalF64, evalJson, evalAll. |
Server POST /evaluate{,/all} | Top-level body field "include_testing": true. No auth-scope change. |
ExdRemote (no-WASM) | Same includeTesting field passes through to the server's body. |
What linters and telemetry see
- Lint emits
E039whentesting = trueis paired with norules— the gate has nothing to gate, almost always an editing mistake. [flag.environments._]cannot declaretesting = true._is the catch-all for non-opted-in traffic in every other env; turning it into a testing env would have no audience. Lint emitsE039fortesting = trueon_.- Telemetry records carry the resolved
rule_matchedexactly as resolution computes it; non-opted-in callers in a testing env land in step 2 / step 4 just as they would in a non-testing env. There is no special "testing-gated" reason variant.
Where testing does NOT change behavior
bucketing_attribute resolution (the identifier the SDK uses for bucket segments) walks every rule's segment graph regardless of testing. The bucket the SDK picks for a given entity is the same whether or not the caller passes include_testing. Telemetry consistency for opted-in vs. non-opted-in callers depends on this; do not "fix" it.
Missing environment blocks
A missing [flag.environments.<env>] for a named env is not an error condition. Resolution skips steps 1–2 (no env block to consult) and falls through to steps 3–4 (_'s rules and default). Authors add an env block only where behavior actually differs from _.
In typed-env mode, the linter does NOT warn on missing per-flag env blocks; _ provides the answer for every undeclared env.
A flag with no env blocks at all is E037 — [flag.environments._] is missing.
A multi-environment worked example
schema_version = "0.1"
[flag]
type = "boolean"
description = "Routes /checkout to the redesigned flow"
owner = "payments"
lifecycle = "active"
[flag.variants]
on = true
off = false
# Catch-all: off everywhere, with internal employees opted in.
[flag.environments._]
variant = "off"
[[flag.environments._.rules]]
description = "Internal employees"
segment = "internal-employees"
variant = "on"
# Pre-prod: always on.
[flag.environments.development]
variant = "on"
[flag.environments.staging]
variant = "on"
# Production: gradual rollout, fully self-contained.
[flag.environments.production]
variant = "off"
[[flag.environments.production.rules]]
description = "10% rollout to general population"
segment = "checkout-redesign-rollout-10"
variant = "on"
Traces:
development, any user → step 2 returns"on".productionnot consulted._not consulted (development declared its ownvariant).staging, any user → same as development.production, inrollout-10→ step 1 returns"on".production, internal employee not inrollout-10→ step 1 no match → step 2 returns"off". Theinternal-employeesrule on_is not consulted.qa(no env block at all) → step 1 and step 2 skipped → step 3 walks_'s rules → internal employees get"on", everyone else gets"off"via step 4.
To make internal employees "on" in production too, duplicate the rule into production's rules array — there is no inheritance from _.
Reading rule_matched in telemetry
Evaluation records carry rule_matched. Possible values:
| Value | Meaning |
|---|---|
"rule:<i>" (env's rules) | Step 1 matched at the <i>-th rule of [flag.environments.<env>].rules. |
"default" (env's default) | Step 2 returned [flag.environments.<env>].variant. |
"rule:<i>" (catch-all rules) | Step 3 matched at the <i>-th rule of [flag.environments._.rules]. |
"default" (catch-all default) | Step 4 returned [flag.environments._].variant. |
"sdk_default" | The SDK could not load the manifest and fell back to the caller's compile-time default. |
"attr_type_mismatch" | A runtime attribute type did not match the manifest's inferred type. See evaluation-context § Inferred attribute types. |
The reason field plus the matched rule's description is usually enough to understand any evaluation. Combine with manifest_version in the record to know which manifest snapshot was in effect.
See also
- flag §
[flag.environments.<env>]blocks — the field reference for env blocks the algorithm walks. - segment — what a rule's segment lookup actually evaluates.
- predicates — the predicate evaluation that backs inline rules and segment predicates.
- evaluation-context — context-attribute lookup, missing-value semantics.
- diagnostics —
E037,E038,E039. - Evaluation parity — the cross-runtime parity fixtures (implementer-facing).
exd eval/exd explain— CLIs that render this walk for one flag.