Ask any system that holds facts about a business three questions. Where did this fact come from? Was it true last March? Who is allowed to see it? A plant model, a supplier master and a customer graph all face the same three, and none of their domain types is about any of them.
This paper is about treating those three as changes to the model rather than as features beside it.
Every domain needs them, none owns them
A domain model describes machines and production lines, or suppliers and contracts. Provenance, change over time and access control cut across every type in it, and that is exactly why nobody models them. They are not part of the domain, so they are left for later, and then they arrive in one of two ways.
The first is application code. Each team adds a source column here, an updated-at timestamp there, a permission check in one service and not in the next. The model never hears about any of it, so a schema review cannot see it, a change to the model does not carry it, and the next consumer has to discover the conventions again, usually by getting them wrong.
The second is a separate product: a lineage tool, a temporal store, an authorisation service. Each keeps its own model of the same entities, joined to the graph by convention. Now there are two descriptions of a supplier, and the join between them is the part nobody owns.
The vocabulary is not the hard part. PROV-O already describes provenance, SOSA observations, QUDT units and OWL-Time intervals, and Google’s published Zanzibar design describes authorisation as a graph of relations. The hard part is landing those vocabularies in one specific model, on its own types, in a way that someone can read before it happens.
An aspect is a planned change, not a feature
GraFlo calls such a vocabulary an aspect: a set of types and relations that every domain needs and none owns, grounded in a standard, which a manifest is lifted with. An aspect has three parts:
- A vocabulary — the types and relations it adds, each carrying the IRI of the standard term it denotes.
- A spec — what the caller must declare because no schema says it: which properties change over time, which unit a float is in, which types carry access labels.
- A planner — a pure function from the manifest and the spec to a list of the same evolution operations described in version control for world models. It applies nothing.
One command, graflo lift, writes that list out as a document before anything is applied. Two properties of the planner matter more than the list itself. First, it refuses to guess. A planner can see structure — an edge that leaves its direction unstated, a float with no unit — but not meaning. Where it would have to invent meaning, it stops and names the missing declaration. Asked to treat a machine’s key as a fact that changes over time, it says:
lift refused: 'Machine': ['machine_id'] identify the type, so they
cannot also be facts that change over time -- an identity that
changes is not an identitySecond, each aspect plans on its own. A text pipeline that needs only provenance takes only provenance; a manifest that needs access labels does not have to take states and observations with them. Named together, the aspects compose, and additive operations come first and removals last, so a reviewer sees everything that is added before anything that is taken away.
One boundary applies to every aspect. A lift changes the contract, not the data flow: it declares the new types, and nothing writes to them until a pipeline is wired to fill them. That is an ordinary resource in the same manifest, added afterwards.
Provenance: an address, never the content
The provenance aspect adds two PROV-O types and two PROV-O relations, and needs no spec at all: the structure is the same in every domain.
A source record is the record a fact was read from — a document, a message, an API response, a table row. It carries where the record is, its media type, when it was retrieved and a hash of what it held. What it does not carry is deliberate: there is no field for the content. The text or the row stays where it was landed — a lake, a table, a file — and the graph points at it. That keeps the graph from becoming a second copy of every message it was ever built from, with nobody governing the copy. Retention and deletion stay where the content lives, and the hash lets a reader check that the record at the address is still the one the fact was read from.
Two relations make lineage expressible. One points from a fact to the record it was derived from. The other points from a record to the agent that produced it — a person, a team, a sensor, a system — and carries a role on the edge, because the same agent authors one record and reports another. Where a manifest already has a type of the same name, the planner refuses and asks for it to be renamed first, or for the aspect to be left out if that type already serves the role.
State and time: facts that hold over an interval
In the plain plant model a machine has a status and an operating temperature, stored on the machine. Each holds one value. When the status changes the old value is gone, and nothing says when a value was true or in which unit the temperature is given.
The state aspect needs a spec, because those are statements of meaning no schema contains:
stateful:
Machine: [status]
ProductionLine: [operating_mode]
observed: [Machine]
measured:
Machine.operating_temp: CelFrom those three declarations the planner writes:
- A state type — the machine’s key, its status and a validity interval, linked to the machine it describes. A state is keyed on the machine’s key plus the start of its interval, because one machine has a different status in each interval and those are different facts, not revisions of one.
- An observation type — one measurement: which property, the value, its unit and the time. The unit travels with each row, because one observation type holds temperatures and pressures alike and no single unit in its schema would be correct.
- A unit on the existing property — the operating temperature is declared in degrees Celsius, added to whatever grounding the field already had.
- A removal, last — the status leaves the machine, since a fact that changes over time does not belong on the thing it is about. The spec can instead keep it as a current value beside the history, and the plan simply ends earlier.
The groundings are chosen to stay true when the model is projected to OWL: the interval columns are grounded in PROV-O, because OWL-Time’s interval ends range over instants rather than timestamps; observations map to SOSA and units to QUDT.
What ships is the declared model: intervals, observations and units as reviewable parts of the contract — the model layer a twin is built on. Writing a new interval is the job of the pipeline that fills the state type, and a point-in-time read is not offered.
Access: a label on every element, checked on every read
Sources come with permissions. A private channel is readable by its members, a restricted ticket by the people who hold the restriction. Once those sources land in one graph, the graph must not become the place where the boundaries disappear — and a graph is harder to protect than a document index. A fact can come from several records with different readers. A traversal leaks structure: a path through a hidden vertex, a count, or a filter that matches each reveals something without returning the hidden element.
Two ideas divide the work. Zanzibar’s relation model is the right authority for who may read what: principals, groups and containers as a graph of relations. But it answers one object at a time, and checking every element a traversal reaches, one at a time, is the wrong shape for a graph read. So enforcement uses labels instead: every element carries the zones it is visible in, and the read filters on them, so a hidden element is never found rather than found and then removed.
The access aspect lifts that label onto the types and relations the spec names. For each vertex type the spec chooses how zones combine: an element that one record defines takes its zones from the latest write, while an element that many records name keeps every zone written to it. A relation always takes the zones of its latest write.
The label is a reserved field. It is set only by the writer, from a trusted stamp; it overwrites any value a document carries, and the writer refuses to write a labelled type without one. The field never appears in what an agent is shown of the schema or of an element, and a query that names it is refused — so an agent cannot see the label, filter on it or set it.
Reading is the other half. A read guard is a trusted filter passed to the read itself — neighbours, traversals, entity resolution, candidate lookup — never a field of the query a caller sends. It admits an element whose zones share a member with the zones the reader opens, and it is applied to every node and relationship of a path, so a short path through a hidden vertex is simply not a path. An element with no zones matches no reader: deny is the default, not a configuration. A backend that cannot apply the guard refuses a guarded read rather than answering more than it should; which backends apply it is in the GraFlo documentation.
Policy is not contract. Which principal opens which zone is decided by the commercial GraFlo platform. The open-source core names no zone, no provider and no ownership class; it carries zone ids, a merge rule and a guard. How an agent’s reads are governed is the subject of governed context for AI agents.
Why operations and not code
Every one of these could have been a feature: a provenance module, a temporal mode, an access plug-in, each with its own switches. The reason they are operations instead is what operations already are.
- Reviewed before applied. The plan is a document that can be read, diffed and checked in while the model is untouched. A reviewer sees the removal of a property from the machine as an entry, not as a side effect buried in a module.
- Fingerprinted. A lifted manifest is a version like any other, identified by a fingerprint of its content, so “is production running the lift we approved?” has a mechanical answer.
- Inverted. Every reversible operation carries a generated inverse, and the ones that destroy information say so. The plant example’s lift inverts without a single blocker and returns the contract to the fingerprint it started from. The inverse restores the model, not data written under it in the meantime.
- Merged. A lift lands on slots like any other operation, so one team can lift a shared model with provenance while another adds properties, and the three-way merge combines them, putting only a genuine collision to a person.
Code bolted on beside the model gets none of this for free. It is reviewed as code by whoever reads that service, and the next team that touches the model cannot see that it exists.
What to ask of any provenance or access layer
Questions worth putting to any candidate, including ours:
- Is provenance in the model, where a schema review sees it, or in application code?
- Does the graph hold a copy of the source content, or an address and a hash of it?
- When a fact changes, is the old value closed over an interval or overwritten?
- Does a measurement carry its unit — per row, where one type measures several things?
- When the layer would have to guess a meaning — a unit, a key, which property changes — does it refuse, or guess?
- Is access checked on every element of a path, or only on the final result?
- Can a caller see, filter on or set the access label?
- Does a backend that cannot enforce access refuse the read, or answer anyway?
- Can the change that added all of this be reviewed before it is applied, and undone afterwards?
The operations underneath every lift — typed, replayable, fingerprinted and mergeable — are the subject of version control for world models. The engine that plans and applies them is GraFlo, open source under Apache-2.0, and the vocabulary used throughout this paper is in the glossary.

