Skip to main content
graph reads four kinds of file it did not write in the same release: config.toml, plan documents, tool documents, and the data directory. Each carries a file version — an integer, separate from the binary’s version and from each other — that moves every time that file’s schema changes. graph version prints the binary version and the file version each kind is written at:
Each file kind has a support window: the oldest version this binary still reads through the one it writes. The line shows the window whenever it is wider than one version (config 3 (reads 2-3)), and graph version --json reports oldest and current under fileVersions.

The compatibility promise

  • Every schema change bumps the file version. A new optional key, a removed key, a rename, a changed shape: all of them. The version is the schema’s generation number, so two binaries that agree on it agree on every key.
  • A binary reads every version in its window, and nothing outside it. Older files inside the window are migrated in memory as they load; the file on disk is not modified until you run a migrate command. Upgrading graph within a window never requires touching a file first.
  • A file outside the window fails, by name, before the schema is consulted. The message says which file, which version it found, which versions this binary reads, and what to do: upgrade graph for a newer file, or migrate an older file with a release that still reads it. Never an “unknown field” for a key that is correct.
  • The window narrows only at a major release, announced one minor release ahead. graph config check reports files that would fall outside it.
Version 1 is the schema graph has always read; introducing the versions changed nothing for existing files.

What a bump carries

A migration is a forward-only step from version N to N+1. The binary carries the chain for its whole window, so a version 2 file on a binary that writes version 4 runs two steps; each step is verified by a frozen fixture pair in the repository (the version N file and the version N+1 file it must produce), and the fixtures for every version in the window must still load on every release. Files graph writes always carry the current version. A plan re-saved from a newer workbench therefore carries the newer number even if it uses nothing new, and an older binary outside that window refuses it — the number is the contract, not the keys the file happens to use.

Config

Both layers are migrated independently, before they merge: the global file and the project file may sit at different versions. The version key itself never reaches the merged config.
migrate preserves comments and layout; it writes version as the first key and leaves everything else where it was. A file already at the current version is left untouched. On a terminal, any command that loads a config file older than the current version prints a one-line hint to run migrate; non-interactive runs (CI, graph mcp serve) stay silent, since the in-memory upgrade already happened.

Plans and tools

A plan file outside the binary’s window is skipped with a diagnostic and stays out of the catalog, exactly like a file that fails to parse: plan list --json reports it under skipped, and naming it (plan run, plan show) reports the version mismatch rather than “no plan named”. A tool file outside the window fails the tool catalog by name.
Both rewrite the document through the YAML tree, so field order survives. A leading comment block (the lines before the first key) is preserved verbatim; comments elsewhere in the file do not survive a YAML round-trip, and the command says how many it dropped. Files written by graph itself — the workbench’s save, plan new, plan draft, plan show — always carry the current version. Every plan and tool file is read strictly: an unknown key anywhere in a step or a tool document is a load error naming the key, never silently ignored. A misspelled timeout_secs used to be dropped on the floor; now the file is skipped and the diagnostic says why.

Data directory

FileStore writes <data_dir>/FORMAT holding the store version on first open and refuses to open a directory whose marker is outside its window. That refusal matters more here than anywhere else: thread messages are appended under a lock without re-reading the file, so an older graph must never write into a layout it does not understand. Records inside the directory carry their own version field for per-record migration.