Skip to main content

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:

  1. A top-level schema_version.
  2. A [flag] table with at least a type field.
  3. A [flag.variants] table with at least one variant.
  4. A [flag.environments._] block declaring variant. Missing _ is E037; declaring _ without variant is E038.

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 herevalue, rules, and enabled were removed in the 0.1 redesign. Every variant-selection field lives inside an [flag.environments.<env>] block.

FieldTypeRequiredDefaultValidationDescription
typestringyesOne of "boolean", "string", "integer", "float", "json" (otherwise E014)The TOML/JSON type of every variant value.
descriptionstringno""Free-form proseWhat the flag controls. The linter emits I002 when absent or empty.
ownerstringnoFree-form, conventionally a team handleTeam or individual responsible for the flag. The linter emits I001 when absent or empty.
lifecyclestringno"active"One of "development", "active", "retired" (otherwise E022)The flag's lifecycle stage. Affects lint diagnostics; does not change evaluation.
tagsarray of stringsno[]Each entry MUST be a stringArbitrary labels for filtering and grouping. No semantic effect.
private_attributesarray of stringsno[]Each entry MUST be a stringContext attributes the SDK MUST strip from this flag's evaluation records. Combined as a union with namespace.private_attributes.

No key field. Schema 0.1 removed flag.key; the flag key is the filename stem and the only source of truth. Declaring key = "..." under [flag] is E016.

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_key field.

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

ValueMeaningLint 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:

  1. What does this flag control? ("Routes /checkout to the redesigned flow.")
  2. 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>
PropertyRule
RequiredAt 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 uniquenessTOML forbids duplicate keys; duplicates raise E001.
Variant valueMUST be type-compatible with flag.type; otherwise E014.
Empty / missing [flag.variants]E020.
Reserved keysNone 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

integer vs. float. A variant declared 0 (TOML integer) is NOT valid for flag.type = "float", even though TOML accepts both 0 and 0.0 syntactically. 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 caseCommon variants
Boolean kill-switchon, off
Boolean experimentcontrol, treatment
Multivariate string experimentcontrol, variant_a, variant_b, …
Numeric configurationdefault, relaxed, strict
JSON configurationdefault, <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]; otherwise E010.

Fields on the block

FieldTypeRequiredDefaultValidation
variantstringrequired on _; optional on named envsMUST be a key in [flag.variants]; otherwise E004. On _, absence raises E038.
rulesarray of tables ([[…rules]])noSame shape as in any env block. See § Rule fields.
testingboolnofalseWhen true, the env's rules are evaluated only for callers that pass eval-time include_testing. Block MUST also declare rulestesting = 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.

FieldTypeRequiredValidationDescription
segmentstringexactly one of segment/predicate MUST be presentMUST reference a segment file in segments/; otherwise E005The segment whose membership is tested.
predicateinline tableexactly one of segment/predicate MUST be presentSame shape and validation as [segment.predicate] — see predicatesAn inline predicate, evaluated as if it were a single-use segment.
variantstringyesMUST be a variant declared in [flag.variants]; otherwise E004The variant returned when this rule matches.
descriptionstringnoFree-form proseOptional 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

IssueDiagnostic
Rule omits both segment and predicate (no audience)E009
Rule omits variantE009
Rule declares both segment and predicateE036 (pick one)
Rule contains deprecated condition / rollout / percentageE013
Rule contains any other unknown fieldE016
Rule's segment = "" (present-but-empty)E005 (lookup fails)
Rule's variant = ""E004
Rule's segment or variant is a non-string valueE026
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

ScenarioDiagnostic
flags/<stem>.toml filename stem violates patternE031 (file dropped before lint)
flag.type missing or unrecognizedE014
flag.description absent or emptyI002
flag.owner absent or emptyI001
flag.lifecycle not one of three permitted valuesE022
flag.lifecycle = "retired" with rulesW002
flag.lifecycle = "retired" without rulesW003 only
flag.tags non-array, or any entry non-stringE001
flag.private_attributes non-array, or any entry non-stringE001
Unknown field at [flag]E016

Variants

ScenarioDiagnostic
[flag.variants] missing or emptyE020
Variant key violates patternE021
Variant value type mismatch with flag.typeE014
Non-finite float in a "float" or "json" variantE029
Rule or env's variant references undeclared variantE004
Variant declared but never referencedW014

Env blocks and rules

ScenarioDiagnostic
[flag.environments._] missingE037
[flag.environments._] present but no variantE038
Named env block references unknown env (typed mode)E010
Env slug malformed (any mode; <env>_)E024
Env block declares no variant and no rulesW016
testing = true with no rules (including on _)E039
testing is non-boolE001
Unknown env-block fieldE016
Rule omits both audience and targetE009
Rule declares both segment and predicateE036
Rule's segment does not existE005
Rule contains deprecated fieldsE013
Flag has no rules at allW003
Two rules in one array share the same segmentW012

  • 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._].variant to 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 owner and description on every flag at creation time. The cost is one line each; the value at the next "what is this flag?" question is high.
  • Update lifecycle deliberately. 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.toml ages better than branch-x-q2-feature.toml.

See also

  • resolution — the four-step algorithm that walks the blocks documented here, plus the testing rollout 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 and W-code mentioned above.
  • examples — cookbook of flag-file patterns.