Relational teams have had schema migration tooling for over a decade. Alembic, Flyway and Liquibase are unglamorous, and they are the reason a relational schema can change on a Tuesday without anyone treating it as an event. Graph and ontology stacks have had almost nothing equivalent, and the consequence is predictable: the model gets built once, carefully, and then quietly calcifies because nobody can establish what changing it would do.
This paper is about what version control looks like when the artifact being versioned is a model of a business rather than a codebase.
The model that cannot change
A knowledge graph that describes a real organisation is never finished. Sources change, systems are replaced, two departments merge and their two notions of “customer” have to become one. Ownership shifts. A field that was free text becomes an enumeration.
The risk here is not the same as application-code risk. A mistake in the model does not break one feature — it propagates to every consumer of the graph at once, and it does so silently, because a graph with a wrong edge still answers queries. The blast radius is what makes teams stop touching the model, and a world model nobody dares change stops describing the world fairly quickly.
So the requirement is not “support schema changes”. It is: make a proposed change reviewable before it happens, make what happened recoverable afterwards, and make the dangerous cases refuse to proceed on their own.
Why a computed diff is not enough
The common approach is to diff two versions of a schema and show the result. This is better than nothing and structurally insufficient, because a diff is a reconstruction rather than a record.
Consider a model where the type Vendor is gone and a type Supplier has appeared with similar fields. A diff can only report the disappearance and the appearance. It cannot distinguish between three quite different events: the type was renamed; the type was deleted and an unrelated one added; or two types were folded together and this is the survivor. Those have different consequences for every downstream consumer, and the information that separates them existed at the moment the change was made — the diff just did not capture it.
The same gap explains why diff-based tooling cannot generate a trustworthy rollback. To reverse a change you need to know what it was, not what it looked like afterwards.
Operations: the change is the artifact
The alternative is to make the change itself the recorded thing. In GraFlo the unit of change is an Operation — a typed, named modification drawn from a vocabulary of more than thirty: adding, renaming, splitting and combining types and properties, retargeting edges, replacing identity policies, changing field types, declaring semantics.
A change between two versions of the model is therefore not a picture to be interpreted. It is an explicit list of operations which, applied to the old model, reproduces the new one exactly. Reviewing a change means reading that list. Approving it means approving that list. Storing the history means storing those lists.
Concretely, an operation list is data — an ordinary YAML document you author, review and check into a repository. Here is the change procurement makes when it decides the DUNS number identifies a supplier: two operations, exactly as they are recorded.
- op: replace_identity
vertices:
supplier:
to:
mode: natural
identity:
- duns
retire: keep
endpoints: follow_new
- additions:
supplier:
- duns_verified
op: add_vertex_propertiesNote what is carried besides the intent. retire: keep says the previous key is demoted to an ordinary property rather than dropped, so existing references can still resolve. endpoints: follow_new says edges that pointed at the old key are repointed at the new one. Those are precisely the decisions a reviewer needs to see and a textual diff cannot express — and they are also what makes the change replayable rather than merely described.
Because the operations are typed rather than textual, three things follow that are not available to a diff:
- A reviewer sees intent. “Rename
VendortoSupplier” and “foldVendorintoSupplier” are different entries, not the same shape of text. - The plan exists before the change does. The operation list can be written out, circulated and approved while the model is still untouched.
- Reversal is computable where it is possible at all. Each reversible operation carries a generated inverse.
That last point needs its qualifier stated plainly, because the temptation to overclaim is strong. Some operations genuinely destroy information: folding two types into one does not record which source each property came from, and narrowing a field's type does not retain the values that no longer fit. For those, no inverse can exist, and generating a rollback that merely appears to restore the prior state would be worse than refusing. They are marked irreversible instead. A system that claims everything is reversible has not thought about the cases where it is not.
Identity and verified replay
A recorded history is only worth having if you can trust that it still describes the model in front of you. Two mechanisms make that checkable rather than assumed.
Content addressing. A version is identified by a fingerprint of the model's own content, computed over a canonical form so that irrelevant ordering differences do not change it. Two models are the same version when they say the same thing, regardless of who produced them, in what order, or through which route. This is what makes “is production running the model we approved?” a question with a mechanical answer.
Verified replay. Each recorded change carries the fingerprint of the model before it and after it. Replaying the history recomputes those fingerprints and compares. A history that no longer describes the model it was generated from fails loudly at the point of divergence, rather than producing a plausible and wrong reconstruction. Loud failure is the entire value: a silent one is indistinguishable from correctness until an auditor asks.
When two teams advance at once
This is where a world model differs most from a codebase, and where single-line versioning breaks down.
A business modelled by twenty teams does not evolve in one line. Procurement owns suppliers, finance owns legal entities and tax, logistics owns shipments, and the platform needs the combination of them. Each advances on its own schedule.
A history that can only be a line has to treat that as an error. It is not an error — it is what actually happened, and a record that cannot represent it is not a record. So parallel work is a first-class recorded fact, and bringing two lines together is reconciled against the point where they last agreed: changes touching different parts of the model combine automatically, and only genuine collisions are put to a person.
The unit of collision is the slot — the addressable location an operation touches, such as vertex/supplier/identity. This is what makes reconciliation precise rather than file-shaped. In the history above, both teams re-keyed supplier, so both changes land on the same slot and collide. Both also added a verification property, and those land on different field slots, so they combine with no question asked.
A conflict is therefore reported with the ancestor's state alongside both sides, because “what did this look like before either change?” is the question a resolver actually needs answered — and the one a two-way diff structurally cannot express:
left : 48e80af2 key suppliers by DUNS
right : 3da08c31 key suppliers by tax ID
base : 2b7fd162
1 conflict(s):
slot : vertex/supplier/identity
reason : both sides changed this slot differently
left : ['replace_identity']
right : ['replace_identity']
base : identity ['id']
merged (took left):
identity : ['duns']
properties: ['id', 'duns', 'tax_id', 'onboarded_at',
'duns_verified', 'tax_id_verified']
hash : 7df433c370c2
merge commit: e0edd207
parents : 48e80af2, 3da08c31
recipe : 9521808c200dThe property list is the part worth reading twice. One side's decision won the contested slot, and both sides' uncontested work survived — duns_verified and tax_id_verified are both present. A last-write-wins registry would have silently discarded one of them, and nobody would have known which.
The recipe on the last line is what makes this compound rather than merely correct once: it records how the disagreement was settled, so the decision does not have to be made again.
The part that compounds is what happens next. The decisions taken in that reconciliation are stored with it. When one side later moves on, the combination is re-derived by replaying what was recorded along with the resolutions already agreed, surfacing only the genuinely new conflicts. Without that, every advance of one model means redoing the join by hand — which is the mechanism by which a shared world model quietly becomes one team's full-time job, and then stops being shared.
Risk, and refusing by default
Not all changes carry the same consequence. Adding an optional property is not the same as replacing the identity policy that determines what counts as the same entity.
Operations therefore carry a risk rating, and the executor refuses anything above the lowest band unless it has been explicitly authorised. The point is not that dangerous changes are forbidden — sometimes the identity policy genuinely is wrong and must be replaced. The point is that such a change cannot happen by momentum, as a side effect of a routine run that nobody read closely.
Identity changes deserve their own note, because they are the case most often underestimated. Changing what identifies an entity is not a schema edit with a data consequence; it is a data problem wearing a schema edit's clothing, since every existing key and every reference to it has to be reconciled. Treating it as ordinary DDL is how organisations get silently duplicated entities.
This is also why identity is worth declaring richly in the first place, so that the common case never needs a re-key at all. Where sources identify the same entity by different evidence, an identity funnel states the fallback order once, and a row is keyed by the strongest evidence it happens to carry:
identity: [id]
identity_funnel:
digest: sha256
include_branch_id: true
branches:
- id: duns
when_all_present: [duns]
fields: [duns]
- id: tax
when_all_present: [tax_id, country]
fields: [tax_id, country]
- id: weak
when_all_present: [name, country]
fields: [name, country]The first branch whose fields are all present wins. Because include_branch_id records which rule keyed a given row, a later argument about why two records did or did not fuse is answerable from the data rather than from memory.
Where the model plane ends
Worth stating precisely, because it is the question a careful reader asks next.
Everything above governs the model — the contract that describes the graph. That is deliberately where the emphasis sits, because it is where the risk concentrates: a mistake in the contract reaches every consumer, while a mistake in one materialised database reaches one database.
Carrying an approved change set further down — compiling it into the exact statements a given backend would execute, rated against what that engine supports natively — is the natural continuation of the same contract, and where a dry run stops describing the plan and starts previewing the action. Governing the model is the part that pays first, which is why it is the part that is built out.
What to ask about model change
Questions worth putting to any candidate, including ours:
- When two versions of the model differ, do I see a computed diff, or the actual change that was made?
- Can a change be reviewed and approved before it is applied to anything?
- Which changes can be undone, and does the system tell me plainly which cannot — or does it claim everything is reversible?
- If the recorded history no longer matches the model in front of me, does replay fail loudly or produce a plausible answer?
- What happens when two teams change the model in parallel? Is that an error, or a recorded fact?
- When one side later advances, do I re-argue the earlier decisions or are they replayed?
- Is a destructive change refused by default, or applied because nobody read the plan closely?
The model, meanwhile, is a file you own. It is portable across engines — the subject of database independence for knowledge graphs — and servable to agents under policy, the subject of governed context for AI agents. The engine that implements all of it is GraFlo.

