flags/<flag-key>.toml
Each flag lives in its own TOML file inside flags/. The filename stem is the flag key — no flag.key field. This page is the complete reference for a flag file: metadata, variants, environment blocks, and rules.
The flag's variant selection algorithm — how a (flag, env, context) triple resolves to one variant — lives in resolution.
Skeleton
schema_version = "0.1"
[flag]
type = "<boolean|string|integer|float|json>"
description = "..."
owner = "..."
lifecycle = "<development|active|retired>"
tags = ["...", "..."]
private_attributes = []
[flag.variants]
# variant_key = value
[flag.environments._]
variant = "<variant-key>" # mandatory: catch-all default
[[flag.environments._.rules]]
# segment OR predicate, plus variant
[flag.environments.<env>]
# variant, rules, testing — each optional
A complete flag file MUST contain:
- A top-level
schema_version. - A
[flag]table with at least atypefield. - A
[flag.variants]table with at least one variant. - A
[flag.environments._]block declaringvariant. Missing_isE037; declaring_withoutvariantisE038.
A flag MAY omit named env blocks entirely; every undeclared env then resolves through _. A flag with no rules in any env block emits W003.
[flag] metadata
This section covers the metadata fields directly under [flag]. No variant-selection fields live here — value, rules, and enabled were removed in the 0.1 redesign. Every variant-selection field lives inside an [flag.environments.<env>] block.
| Field | Type | Required | Default | Validation | Description |
|---|---|---|---|---|---|
type | string | yes | — | One of "boolean", "string", "integer", "float", "json" (otherwise E014) | The TOML/JSON type of every variant value. |
description | string | no | "" | Free-form prose | What the flag controls. The linter emits I002 when absent or empty. |
owner | string | no | — | Free-form, conventionally a team handle | Team or individual responsible for the flag. The linter emits I001 when absent or empty. |
lifecycle | string | no | "active" | One of "development", "active", "retired" (otherwise E022) | The flag's lifecycle stage. Affects lint diagnostics; does not change evaluation. |
tags | array of strings | no | [] | Each entry MUST be a string | Arbitrary labels for filtering and grouping. No semantic effect. |
private_attributes | array of strings | no | [] | Each entry MUST be a string | Context attributes the SDK MUST strip from this flag's evaluation records. Combined as a union with namespace.private_attributes. |
No
keyfield. Schema 0.1 removedflag.key; the flag key is the filename stem and the only source of truth. Declaringkey = "..."under[flag]isE016.
Flag key semantics
The filename stem is the wire-level identifier for the flag in:
- The SDK evaluation API:
client.bool_flag("checkout-redesign", &ctx, false). - Telemetry events: every per-evaluation record carries the flag key as
exd.flag_key. - Audit logs and the
EvaluationResult.flag_keyfield.
The stem MUST match the key pattern [a-z][a-z0-9_-]* (max 63 chars). Violations raise E031 and the file is dropped before per-file lint.
lifecycle semantics
| Value | Meaning | Lint effect |
|---|---|---|
"development" | Under construction. | None. |
"active" (default) | In use. | None. |
"retired" | Scheduled for removal. | W002 when any rule path exists; W003 when no rules exist (always emitted, regardless of lifecycle). |
Lifecycle has no effect on evaluation. A retired flag still evaluates normally; the warning is a signal to engineers to delete callers from code and remove the flag.
description and owner
Both are free-form prose. description SHOULD answer two questions:
- What does this flag control? ("Routes /checkout to the redesigned flow.")
- Why does it exist? ("Introduced for the Q2 2026 redesign; retained as a permanent kill switch.")
owner is conventionally a Slack handle, GitHub team, or email. Format is not enforced.
The linter emits info diagnostics I001 / I002 when these are absent or empty — informational only, no upload-blocking.
tags
An array of strings. The linter does not validate length, content, or uniqueness; dashboards typically deduplicate at display time. Conventionally lowercase, hyphenated, descriptive: ["checkout", "experiment", "q2-2026"].
private_attributes
Context-attribute names the SDK MUST strip from this flag's evaluation records before sink delivery. The match is byte-exact on the attribute name; values are not inspected.
The effective set for a record is the union of namespace.private_attributes and this flag's private_attributes — declaring an attribute on the flag does not unset the namespace-level entry, and vice versa. Duplicates are accepted (deduplication happens at filtering time).
This field has no effect on evaluation behavior. It is read only at record-emission time by the telemetry layer.
[flag.variants] table
Every flag MUST declare at least one variant. Variants are scalar mappings of <key> = <value>.
[flag.variants]
<variant-key> = <value>
<variant-key> = <value>
| Property | Rule |
|---|---|
| Required | At least one variant; otherwise E020. |
| Variant-key pattern | [a-z][a-z0-9_-]*, max 63 chars; otherwise E021. Same class as flag and segment keys. |
| Variant-key uniqueness | TOML forbids duplicate keys; duplicates raise E001. |
| Variant value | MUST be type-compatible with flag.type; otherwise E014. |
Empty / missing [flag.variants] | E020. |
| Reserved keys | None reserved at the schema level. Conventional names listed below. |
Variant order in the source is not preserved across server canonicalization. Do not rely on declaration order at evaluation time.
Variant table-form is reserved
In schema 0.1 the only valid variant form is the scalar map above. A richer table-form ([flag.variants.<key>] with nested value / description fields) is reserved for a future minor version and is rejected today as E014.
For per-variant prose, use the flag's top-level description, a TOML comment above the variant, or external documentation.
Per-type variant rules
flag.type = "boolean"
Variant values MUST be TOML booleans. Conventional names: on, off.
[flag]
type = "boolean"
[flag.variants]
on = true
off = false
Boolean flags MAY declare more than two variants if multiple distinct outcomes are useful for telemetry differentiation, but the SDK's bool_flag accessor returns the single boolean value of whichever variant was selected.
flag.type = "string"
Variant values MUST be TOML strings. Conventional names for multivariate: control, variant_a, variant_b, ….
[flag]
type = "string"
[flag.variants]
control = "Payments made simple."
variant_a = "Send money in seconds."
variant_b = "The fastest way to pay."
No schema-enforced maximum string length; for long strings use flag.type = "json".
flag.type = "integer"
Variant values MUST be TOML integers. Signed 64-bit. Negatives permitted; values outside [−2⁶³, 2⁶³−1] fail TOML parsing (E001).
[flag]
type = "integer"
[flag.variants]
default = 10
internal_test = 100
flag.type = "float"
Variant values MUST be TOML floats. IEEE 754 double precision, finite only. Non-finite literals (nan, inf, -inf) are forbidden — E029.
[flag]
type = "float"
[flag.variants]
default = 0.10
aggressive = 0.50
integervs.float. A variant declared0(TOML integer) is NOT valid forflag.type = "float", even though TOML accepts both0and0.0syntactically. SDKs treat the declared type as the source of truth. Always include a decimal point on float variant values.
flag.type = "json"
Variant values MUST be inline TOML tables or arrays. Scalars (strings, numbers, booleans) declared as JSON-flag variants raise E014 — to express a scalar JSON value, declare flag.type as the corresponding scalar type instead.
[flag]
type = "json"
[flag.variants]
default = { tier = "free", per_minute = 60, per_day = 10000 }
pro = { tier = "pro", per_minute = 600, per_day = 100000 }
The TOML inline-table form is the canonical wire representation: reviewable, parses without an extra layer of escaping, and round-trips through exd manifest pull/push losslessly. SDKs deliver the parsed JSON value via json_flag().
Non-finite floats are forbidden at every depth. A "json" variant's table or array MUST NOT contain nan, inf, or -inf anywhere — top level, nested tables, or arrays. The linter walks recursively and raises E029 on the first non-finite float. JSON has no native representation for these values; permitting them would force every SDK to invent a wire encoding with no portable answer. Nested integers, booleans, and strings inside a "json" variant are unconstrained.
Variant key conventions
| Use case | Common variants |
|---|---|
| Boolean kill-switch | on, off |
| Boolean experiment | control, treatment |
| Multivariate string experiment | control, variant_a, variant_b, … |
| Numeric configuration | default, relaxed, strict |
| JSON configuration | default, <environment-tier> |
[flag.environments.<env>] blocks
A flag's behavior at evaluation time is determined by exactly one [flag.environments.<env>] block per evaluation. When that block declines a piece of the answer, resolution falls through to the catch-all block [flag.environments._]. There is no field-level inheritance; the flag table itself carries only metadata.
The full resolution algorithm with worked examples lives in resolution. This section documents the block's fields.
Block shape
[flag.environments.<env>]
variant = "<variant-key>" # optional; fallback for this env
testing = false # optional; gates rule evaluation on caller opt-in
[[flag.environments.<env>.rules]]
description = "..."
segment = "<segment-key>" # OR predicate = { ... }
variant = "<variant-key>"
<env> is either:
- The reserved name
_(the catch-all), or - An environment slug matching
[a-z][a-z0-9-]*. In typed-env mode, the slug MUST also appear in[namespace.environments]; otherwiseE010.
Fields on the block
| Field | Type | Required | Default | Validation |
|---|---|---|---|---|
variant | string | required on _; optional on named envs | — | MUST be a key in [flag.variants]; otherwise E004. On _, absence raises E038. |
rules | array of tables ([[…rules]]) | no | — | Same shape as in any env block. See § Rule fields. |
testing | bool | no | false | When true, the env's rules are evaluated only for callers that pass eval-time include_testing. Block MUST also declare rules — testing = true with no rules is E039. Non-bool produces E001. |
enabled, value, and other previously recognized fields are removed. Authors who want a kill-switch effect declare variant pointing at the off-variant and omit rules. A block declaring neither variant nor rules produces W016 so the author can clean it up.
Unknown fields
The env block has a closed field set: variant, rules, testing. Any other key (e.g., valeu, default_variant, enabled) is E016. The default_variant typo is called out specifically with a hint pointing at variant.
The _ catch-all
_ is the env block that applies when the requested environment did not supply a piece of the answer. It MUST exist, and it MUST declare a variant. Its rules array is optional.
_ is reserved as an env name regardless of mode (typed-env 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.
Rule fields
A rule MUST identify a target audience (exactly one of segment or predicate) and a variant. The same fields and validation apply to every rule, whether it lives in _'s array or a named env's array.
| Field | Type | Required | Validation | Description |
|---|---|---|---|---|
segment | string | exactly one of segment/predicate MUST be present | MUST reference a segment file in segments/; otherwise E005 | The segment whose membership is tested. |
predicate | inline table | exactly one of segment/predicate MUST be present | Same shape and validation as [segment.predicate] — see predicates | An inline predicate, evaluated as if it were a single-use segment. |
variant | string | yes | MUST be a variant declared in [flag.variants]; otherwise E004 | The variant returned when this rule matches. |
description | string | no | Free-form prose | Optional human-readable label. Strongly recommended for production rules. |
Inline predicates
A rule MAY replace segment = "..." with predicate = { ... } to declare its targeting inline. The predicate body has the same shape as a segment's predicate — atom, compound (and / or / not), or segment reference, with all the operators in predicates. Inline predicates are convenient for one-off rules that don't earn a named segment; named segments are the right call when an audience appears in more than one rule.
[[flag.environments._.rules]]
variant = "on"
predicate = { and = [
{ attribute = "user.country", op = "eq", value = "US" },
{ attribute = "user.plan", op = "in", values = ["pro", "ent"] }
]}
Linter rules on rules
| Issue | Diagnostic |
|---|---|
Rule omits both segment and predicate (no audience) | E009 |
Rule omits variant | E009 |
Rule declares both segment and predicate | E036 (pick one) |
Rule contains deprecated condition / rollout / percentage | E013 |
| Rule contains any other unknown field | E016 |
Rule's segment = "" (present-but-empty) | E005 (lookup fails) |
Rule's variant = "" | E004 |
Rule's segment or variant is a non-string value | E026 |
Two rules in the same array declare the same segment (silent shadowing) | W012 per shadowed rule |
Worked examples
Minimal flag
# flags/welcome-banner.toml
schema_version = "0.1"
[flag]
type = "boolean"
[flag.variants]
on = true
off = false
[flag.environments._]
variant = "off"
[[flag.environments._.rules]]
description = "US users see the banner"
variant = "on"
predicate = { attribute = "user.country", op = "eq", value = "US" }
A single file. No namespace.toml, no segments/, no named env blocks. The _ block carries the rule and the fallback. Same flag is valid against an untyped-env flag namespace and a typed-env one — _ is reserved regardless of mode.
Boolean kill-switch with per-env override
# flags/checkout-redesign.toml
schema_version = "0.1"
[flag]
type = "boolean"
description = "Routes /checkout to the redesigned flow introduced in Q2 2026."
owner = "payments-team"
lifecycle = "active"
tags = ["checkout", "kill-switch"]
[flag.variants]
on = true
off = false
[flag.environments._]
variant = "off"
[[flag.environments._.rules]]
description = "Internal employees and beta program users get the redesign first"
segment = "internal-or-beta-users"
variant = "on"
[[flag.environments._.rules]]
description = "10% rollout to general population"
segment = "checkout-redesign-rollout-10"
variant = "on"
# Always on in development and staging — fully self-contained, no rules.
[flag.environments.development]
variant = "on"
[flag.environments.staging]
variant = "on"
The _ block carries the production-shaped behavior (off by default, opt-in for two segments). development and staging declare variant = "on" with no rules; they are fully self-contained and _ is never consulted for those envs. To kill the rollout in production without losing the rule set, add [flag.environments.production] with variant = "off" and no rules.
Common diagnostics
Metadata
| Scenario | Diagnostic |
|---|---|
flags/<stem>.toml filename stem violates pattern | E031 (file dropped before lint) |
flag.type missing or unrecognized | E014 |
flag.description absent or empty | I002 |
flag.owner absent or empty | I001 |
flag.lifecycle not one of three permitted values | E022 |
flag.lifecycle = "retired" with rules | W002 |
flag.lifecycle = "retired" without rules | W003 only |
flag.tags non-array, or any entry non-string | E001 |
flag.private_attributes non-array, or any entry non-string | E001 |
Unknown field at [flag] | E016 |
Variants
| Scenario | Diagnostic |
|---|---|
[flag.variants] missing or empty | E020 |
| Variant key violates pattern | E021 |
Variant value type mismatch with flag.type | E014 |
Non-finite float in a "float" or "json" variant | E029 |
Rule or env's variant references undeclared variant | E004 |
| Variant declared but never referenced | W014 |
Env blocks and rules
| Scenario | Diagnostic |
|---|---|
[flag.environments._] missing | E037 |
[flag.environments._] present but no variant | E038 |
| Named env block references unknown env (typed mode) | E010 |
Env slug malformed (any mode; <env> ≠ _) | E024 |
Env block declares no variant and no rules | W016 |
testing = true with no rules (including on _) | E039 |
testing is non-bool | E001 |
| Unknown env-block field | E016 |
| Rule omits both audience and target | E009 |
Rule declares both segment and predicate | E036 |
Rule's segment does not exist | E005 |
| Rule contains deprecated fields | E013 |
| Flag has no rules at all | W003 |
Two rules in one array share the same segment | W012 |
Recommended practice
- Start with
[flag.environments._]only. Add named env blocks only when behavior actually differs from the catch-all. A flag with one rule and one default does not need three identical env blocks. - Make
_default-safe. Set[flag.environments._].variantto the off-variant (or its multivariate equivalent). Default-off is the safe pattern: every audience opts in via an explicit, reviewable rule. - Use named segments for reusable audiences, inline predicates for one-offs. A predicate copy-pasted across three rules is a segment waiting to be named.
- Order rules from most specific to least specific. Catch-all rules at the top mask narrower rules below them.
- Always populate rule
description. Two months later, the description is the only place a reviewer can quickly confirm what audience a rule was meant to target. - Set
owneranddescriptionon every flag at creation time. The cost is one line each; the value at the next "what is this flag?" question is high. - Update
lifecycledeliberately. Promote from"development"to"active"when the flag is wired into production for the first time; promote to"retired"once rollout has fully landed and the surrounding code can be deleted. - Pick the flag filename carefully. The stem is the key, immutable in practice (renaming requires a coordinated SDK and rule update). Prefer descriptive verbs and nouns over implementation details.
checkout-redesign.tomlages better thanbranch-x-q2-feature.toml.
See also
- resolution — the four-step algorithm that walks the blocks documented here, plus the
testingrollout gate. - segment — the segments that flag rules reference.
- predicates — the predicate language used in inline rules and in segments.
- namespace — env declarations +
private_attributes(the namespace-level half of telemetry filtering). - diagnostics — every
E-code andW-code mentioned above. - examples — cookbook of flag-file patterns.