Schema versioning
Every file in a flag namespace declares its own schema_version. This page is the reference for that field, the compatibility model, and the migration tooling.
The schema_version field
Every TOML file in a manifest — namespace.toml, every flags/<flag-key>.toml, and every segments/<segment-key>.toml — MUST contain a top-level schema_version key, declared early in the file by convention.
schema_version = "0.1"
Field rules
| Property | Rule |
|---|---|
| Type | TOML string. |
| Format | <major>.<minor> — two non-negative integers separated by a single . (e.g., "0.1", "1.0", "1.3", "2.0"). |
| Required | Yes, on every file. |
| Position | Top-level. MUST NOT be nested under any TOML table. |
| Linter code on missing/invalid | E001. |
The linter accepts any string whose components parse as non-negative integers separated by exactly one dot. Patch-level versions ("1.0.1"), prefix qualifiers ("v1.0"), trailing build metadata ("1.0+abc"), and non-numeric components ("1.x", "abc") all fail validation.
Major 0 is reserved for pre-stable iteration (0.1, 0.2, …). The current schema is 0.1. The first stable wire-format major will be 1.0.
What the linter validates
The linter validates the shape of the version string. It does NOT enforce that the major matches a server-supported value — a file declaring "99.0" passes lint locally but is rejected at upload time with schema_version_mismatch. The server is the authority on which majors it accepts.
Use exd lint --schema-major <N> to opt into a local major check.
Compatibility model
Minor versions are additive
A newer minor version within the same major is forward-compatible: a file declaring schema_version = "1.3" MUST be accepted by any implementation that supports "1.0" or higher in the 1.x line. This is what allows a team to upgrade their CLI before the central server, or vice versa, without breaking uploads.
A flag namespace MAY mix minor versions across files (for example, namespace.toml at "1.0" and a single flag file at "1.2"). The linter does NOT require every file to declare the same version. Mismatched minors within one flag namespace are accepted with W008 so authors notice the drift.
Major versions are not compatible
A file whose major version differs from the server's supported major is rejected at upload with schema_version_mismatch (HTTP 422). Mixing majors within a manifest is unsupported; uploading such a manifest fails with the same code.
The linter does NOT gate on major-version mismatch; it accepts any well-formed major.minor. This is intentional — local linting against the wrong version is useful information ("file parses, but the wire format will reject it"), and gating would force the linter to track the server's supported version.
Forward-compat policy
Forward compatibility — silently accepting unknown fields a newer minor version may have introduced — is opt-in per site, not a blanket default. Most tables in the schema have a closed set of recognized fields; an unrecognized field raises E016. This catches typos that would otherwise silently break evaluation (default_varient = "off", stat = 0) and forces every minor-version field addition to be reflected in the spec.
Forward-compatible sites in the current schema:
| Site | Behavior |
|---|---|
Env values under [namespace.environments] (each <env-slug> = { ... }) | Unknown fields are silently ignored. |
Every other table is strict. The full set of recognized fields per table is enumerated in E016.
Server acceptance policy
| Condition | Server response |
|---|---|
| All files declare a major matching the server's current major | Upload accepted (subject to lint and authorization). |
| Any file's major is below the server's minimum supported major | 422 schema_version_mismatch. |
| Any file's major is above the server's current major | 422 schema_version_mismatch. |
| Any minor version within a supported major | Always accepted. |
schema_version missing or malformed on any file | Linter rejects with E001; server returns manifest_lint_failed. |
Mismatched versions inside one flag namespace
namespace.toml | flag file | Result |
|---|---|---|
"1.0" | "1.0" | Accepted, no diagnostics. |
"1.0" | "1.3" | Accepted with W008. Server (current major 1) accepts the upload. |
"1.0" | "2.0" | Linter passes each file's shape check; server rejects with schema_version_mismatch. |
"1.0" | "1.x" | Linter raises E001 on the flag file; upload rejected. |
Migration
Minor upgrades within a major
exd manifest migrate --from 1.0 --to 1.3 ./payments/
Rewrites every file in the flag namespace to advance the schema_version field and add or rename any optional fields introduced between source and target. Migration is mechanical and lossless: files written by exd manifest migrate parse cleanly under both source and target versions, with schema_version set to the target.
The command refuses to run if any file already declares a higher version than the target, and refuses to run across major boundaries.
Major upgrades
exd manifest upgrade --to 2.0 ./payments/
The only supported path between major versions. Unlike migrate, upgrade MAY rewrite semantics, prompt for human decisions on ambiguous mappings, and produce a manifest that is not readable by older clients. A migration guide is published alongside each major version bump.
Common diagnostics
| Scenario | Diagnostic |
|---|---|
schema_version missing | E001 |
schema_version = 1 (integer, not string) | E001 |
schema_version = "1" (no minor component) | E001 |
schema_version = "1.0.0" (patch component) | E001 |
schema_version = "v1.0" (prefix) | E001 |
schema_version = "1.x" (non-numeric minor) | E001 |
schema_version = "" | E001 |
schema_version = "0.1" | accepted (pre-stable) |
schema_version = "99.0" | accepted by linter; server rejects with schema_version_mismatch |
| Namespace and flag declare different minors within the same major | accepted with W008 |
| Namespace and flag declare different majors | linter passes; server rejects |
Recommended practice
- Default to the current schema major. Today that is
"0.1". The first stable wire-format major will be"1.0". - Keep all files on the same minor. Mixing is allowed, but uniform versions keep
git blamehonest about which version your flag namespace runs on. - Run
exd manifest migrateinstead of hand-editingschema_version. The migrate command also adds or renames fields the new minor introduces — a hand edit would miss those. - Pin a CI lint job to the same
exdversion as production. A divergent CLI may accept a manifest the server rejects for major-version reasons; pinning eliminates that drift.
See also
- diagnostics —
E001,W008. - packaging — the upload path that enforces server acceptance policy.
exd lint— the local validator.exd manifest push— the upload path.