The first essay in this series argued that vector retrieval fails structurally on four axes: it averages contradictions, it forgets time, it loses attribution, and it can't hop. We treated the four as roughly equal in weight, which made the argument easier to follow. In practice one of them, the forgetting of time, is doing most of the damage in production systems today, and it's the one the fewest teams take seriously. This essay takes it in depth: what it means for a knowledge system to model time, the difference between treating time as metadata and treating it as a primitive, and what a "valid_to" timestamp does for an agent's reasoning that no amount of clever prompting can replace.
Start with a thought experiment. Imagine you're building a research assistant for a working scientist. Your assistant has ingested every paper, every transcript, every blog post, every tweet from the field of, say, deep learning architectures over the last decade. A user asks it a question that should be simple: "What does Andrej Karpathy think about whether scaling is sufficient for general intelligence?"
Karpathy has been one of the most public voices in the field for a long time, and he's said different things about scaling at different times. In 2019, when GPT-2 had just landed and the scaling laws were the most exciting thing anyone had seen in years, he leaned in hard on the scaling story. By 2022, with GPT-3 in deployment and the limitations of pure autoregressive prediction starting to bite, he was more measured. By 2024, in his now-famous YouTube lectures, he was explicit that pure scaling has limitations and that structured world models are increasingly necessary for reliable reasoning. By 2026 his position has shifted further, in ways we can debate but can't ignore.
So what does Karpathy think? The accurate answer is: it depends on when you ask, and the system's answer should explicitly include that dependence. An atemporal retrieval system can't give that answer. It will surface whichever of the five quotes happens to have the highest cosine similarity to the user's phrasing, present it as Karpathy's view, and confidently mislead. A temporally aware system can give it. It can return all five, ordered by date, with their validity windows marked. It can return the most recent one and explicitly note that earlier positions existed and were revised. It can answer the meta-question, "how has Karpathy's position evolved?", natively, because the evolution is in the structure.
What "bi-temporal" actually means
The term "bi-temporal" comes from database theory and predates the current AI moment by decades. Database researchers, who deal with a world that changes and with records of that world that also change, figured out a long time ago that a serious data model needs two distinct time dimensions. The first is valid time: when the thing the record describes was true in the world. The second is transaction time: when the record itself was created, updated, or invalidated in the database. These two times are different things, and conflating them is a category error that produces wrong answers.
Take a customer whose address changes from A to B on March 1. Your system finds out about it on March 15. The valid_from of the new address is March 1. The transaction_from is March 15. If on March 10 you ran a query "where does this customer live?", what should the system return? An atemporal database would just store the latest known address, and on March 10 would still return A. A bi-temporal database stores both, knows that A was valid until March 1 and B is valid from March 1, but also knows that on March 10 the system hadn't yet been told about the change. Depending on which question you're asking, "where did the customer actually live on March 10?" vs. "where did our records say the customer lived on March 10?", you get different correct answers.
The example sounds like accountant pedantry, but every claim in a knowledge graph has this same structure. Karpathy held position X. He changed his mind, internally, at some point in 2022. He stated the new position publicly in 2023. Your system ingested the 2023 statement in 2024. The valid_time of "Karpathy holds position X" closes somewhere in 2022. The transaction_time of "the system knows Karpathy no longer holds X" opens in 2024. Between 2022 and 2024, the world had moved on and the system hadn't. The system needs to be able to represent that gap, and to query around it.
Graphiti's bi-temporal contribution
Graphiti is a temporal knowledge graph framework built on top of Neo4j. What sets it apart from a generic graph database is that every node and every edge it stores carries bi-temporal metadata as a first-class property. You don't have to remember to attach timestamps, and you don't have to write custom queries that filter on validity. The framework's API requires you to provide the temporal context at write time, and every read goes through a temporal lens by default.
Concretely, when you add an episode to Graphiti, an episode being a chunk of new information entering the system, the framework runs entity extraction and relation extraction on the episode, identifies which entities and which relationships are mentioned, looks up whether those entities already exist in the graph, and then does some bookkeeping for each new claim. Does the claim agree with existing graph state? If so, the existing claim's evidence count increments. Does it disagree? If so, the existing claim's valid_to may be closed, and the new claim opened. Is it ambiguous, talking about a different aspect, or a different context? Then both are kept, with appropriate context tags. The framework does this work for you, episode by episode, and the result is a graph whose temporal structure reflects the real arrival pattern of information rather than the convenient fiction that all facts arrived at once.
That arrival pattern matters because it is the information. When Karpathy's 2024 position arrives in your system, the fact that it closed his 2019 position signals that the field has been moving, that the prior consensus has shifted, that downstream agents who were relying on the old position need to be invalidated. Graphiti makes this signal queryable: "show me all claims whose validity windows were closed in the last six months." That query, run periodically, is a structural change-detection system for your domain. An atemporal vector store can't produce anything like it, because the information was never there in the first place.
The two-timestamp discipline at ingestion
Bi-temporal modeling puts a discipline on your ingestion pipeline, and in practice it's the step that gets skipped. Every claim that enters the system needs both timestamps attached, and getting them right takes work. The transaction_time is easy: it's whenever your pipeline processed the source. The valid_time is harder, because the source itself may not state it cleanly.
For research papers, the valid_time is usually the publication date, which is right there in the metadata. For YouTube transcripts, it's the upload date of the video. For tweets and blog posts, it's the post date. For books, it's the publication date, which can be tricky if the edition matters (a textbook revised in 2020 has different claims than its 2014 first edition). For interview transcripts, it's the date the interview happened, which may differ from the date the transcript was published. For each source type, your pipeline needs to know where to find the valid_time, and your schema has to hold sources that are precise to the second next to sources that are precise only to the year.
| Source type | Where the pipeline reads valid_time | The catch |
|---|---|---|
| Research paper | The publication date, sitting in the metadata | The easy case, and the one people generalise from |
| YouTube transcript | The upload date of the video | Precise to the day, and the talk may predate it |
| Tweet or blog post | The post date | Precise to the second, which the schema has to allow |
| Book | The publication date of the edition in hand | A textbook revised in 2020 carries different claims than its 2014 first edition |
| Interview transcript | The date the interview happened | Differs from the date the transcript was published, and only one of the two is the answer |
Then there's a second-order problem: a claim made at time T may be about something at a different time. When Karpathy in 2024 says "in 2019 I thought scaling was all we needed," there are two valid_times in play. The claim was asserted in 2024 (valid_from of the assertion). The claim is about Karpathy's 2019 state (valid_from of the referenced position). A serious bi-temporal model represents both. The current state of the graph is that the 2019 position existed, and we know about it because of a 2024 statement. The 2024 statement itself is also a current claim, with its own validity window, and it remains valid until something causes it to be closed.
Those two dates separate a system that can answer "what did Karpathy think in 2019?" with citation chains intact from a system that flattens everything into a single timestamp and gets confused when the question gets specific.
Validity windows and the supersession protocol
The supersession protocol is the heart of bi-temporal knowledge management, because it decides what happens when a new claim arrives that conflicts with an existing one. An atemporal system overwrites. The old fact is destroyed, the new fact takes its place, and the history is lost. A naive temporal system appends: both facts coexist, and the application layer has to figure out which is current. A bi-temporal system with supersession closes the old claim's validity window at the moment the new claim's window opens, and links the two with an explicit SUPERSEDES edge.
The history is only part of the payoff, because the SUPERSEDES edge is itself queryable. You can ask: "show me all positions this author has revised." Or: "which claims in domain D have been most volatile over the last year?" Or: "when did the field's consensus on topic T shift?" These queries are trivial against a graph that maintains supersession chains, and impossible against a graph that doesn't.
Supersession keeps the old claim in the graph, marked as no longer valid. Queries that ask about "the current state of the world" filter it out. Queries that ask about "the historical state of the world as of date D" include it if D falls within its old validity window. That's what makes a bi-temporal graph a time machine: pick any point in its history, ask what it looked like, and you get the actual graph state as it stood then, because the graph never forgot.
The agent's time horizon
Agents that operate on top of a bi-temporal graph have a fundamentally different relationship to time than agents that operate on top of a flat vector store. The vector-store agent lives in an eternal present: every query returns the same kind of results regardless of when it was asked, because the index doesn't change in meaningful ways. The graph agent operates with a time cursor. By default, the cursor is "now," and queries return current state. But the cursor can be moved. A query can be issued as-of a past date, and the graph responds with what was known then. The agent can therefore reason about its own evolution. "What did I believe about this customer six months ago, and what new information has changed my belief?" is a question the agent can answer, because the graph remembers both states.
A time cursor changes what's possible at the application layer. A customer-success agent that has been deployed for a year and that has been ingesting customer feedback continuously can be asked: "this customer is unhappy, what did we know about their preferences when we made the decision that led to this?" The agent runs the query as-of the decision date, gets the historical snapshot, and can produce a meaningful answer. The same query against an atemporal system returns the current state, which may bear no resemblance to what the system believed when the decision was made. The customer-success agent on a bi-temporal substrate can do real root-cause analysis. The one on the atemporal substrate can only confabulate.
What you give up for bi-temporal modeling
Bi-temporal modeling has real costs, and they're operational ones.
| Cost | What grows | Where it is paid |
|---|---|---|
| Storage | Every superseded claim stays, with its old window intact, so the graph accumulates historical state year on year | The storage budget, recoverable by archiving old superseded claims to cold storage once fast querying against them stops mattering |
| Query complexity | Every read either takes the default temporal filter or names a cursor, and the time dimension turns up in every analytics and audit query | Longer Cypher, harder query optimisation, and an engineering surface that has to be staffed |
| Ingestion complexity | Sourcing valid_time from heterogeneous content, handling references to past states, deciding supersession against coexistence | Every episode, and getting it wrong corrupts the graph in ways that are hard to detect and hard to roll back |
Storage grows. Every claim that gets superseded remains in the graph, with its old validity window intact. Over years, the graph accumulates significant historical state. The growth is recoverable, since you can periodically archive old superseded claims to cold storage if you no longer need fast querying against them, but it still costs something. The disk usage of a bi-temporal graph after two years of operation is meaningfully larger than the disk usage of an atemporal one, and the storage budget needs to account for that.
Query complexity grows. Every read against the graph either uses the default temporal filter (current state) or explicitly specifies a time cursor. Application developers have to think about which they want. Most reads will be "current," which is fine, but the moment you start writing analytics queries or audit queries, the time dimension becomes pervasive. Cypher queries get longer. Query optimization gets harder. The Neo4j community has good tooling for this, but it's real engineering work that has to be staffed.
Ingestion pipeline complexity grows. The two-timestamp discipline described above is, by itself, a project. Sourcing valid_time correctly from heterogeneous content types, handling references to past states, and deciding when a new claim supersedes an old one or coexists with it are decisions the pipeline has to make on every episode, and getting them wrong corrupts the graph in ways that are hard to detect and hard to roll back.
Each of these costs is a line item in the engineering budget. Itemizing them tells you what you're signing up for, and you shouldn't sign up for it on a system whose corpus is small, static, and uncontested. For those systems, atemporal storage is fine. The bi-temporal model pays for itself when the corpus is large, dynamic, and contested, which is the corpus an ambitious agentic platform is going to face.
Implementing it without Graphiti
You don't need Graphiti to get bi-temporal behavior. You can implement the patterns directly in Neo4j, or in any property graph database. The cost is that you do the bookkeeping yourself. Every node gets valid_from and valid_to and tx_from and tx_to properties. Every edge gets the same. Every query that asks about "current state" filters for valid_to IS NULL OR valid_to > now(). Every write that supersedes an existing claim is a two-step transaction: close the old claim's valid_to, write the new claim. Get this wrong and your graph is inconsistent.
Graphiti automates the bookkeeping and provides higher-level abstractions that are tedious to implement correctly: add_episode, entity resolution, automatic supersession. For a small team that wants the bi-temporal benefits without staffing a full graph engineering function, Graphiti is the pragmatic choice. For a team with strong graph engineering and specific requirements that Graphiti doesn't yet meet, rolling your own on top of Neo4j is reasonable. The choice is operational, not philosophical. Either way, the data model is what matters.
| Graphiti on Neo4j | Rolling your own on a property graph | |
|---|---|---|
| Temporal metadata | Carried on every node and edge as a first-class property, required at write time | Four properties you attach and maintain yourself on every node and every edge |
| Reads | Pass through a temporal lens by default | Each one filters explicitly on valid_to IS NULL OR valid_to > now() |
| Supersession | Automatic, alongside add_episode and entity resolution | A two-step transaction you write, and an inconsistent graph when it half-lands |
| Who it suits | A small team that wants the bi-temporal benefits without staffing a graph engineering function | A team with strong graph engineering and requirements the framework does not yet meet |
The deepest payoff: time as a moat
A bi-temporal knowledge graph that has been running for a year contains something that is structurally impossible for a competitor to replicate, no matter how much capital they raise or how many GPUs they buy: a year's worth of historical state, accurately recorded. The supersession chains, the validity windows and the temporal evolution of every claim in the corpus are all there and all queryable, and they're the basis of any analytics, any audit, any "what changed and when" question.
A competitor who starts a year later can't synthesize that year. They can ingest the same sources, sure, but they'll only have the current state, because the evolution happened in real time and the competitor wasn't there to record it. They'll know where things ended up and nothing about how they got there. And for many of the most interesting questions (how the field changed, how a person's views evolved, how a market shifted), how things got there is the answer.
That recorded history is the moat bi-temporal modeling creates, because every day of operation adds value that can't be back-filled. Teams that start now, even with imperfect implementations, are pulling ahead of teams that will start in 2027. Teams that start in 2027 will be pulling ahead of teams that start in 2030. The advantage compounds. That moat, beyond the technical merits and the agent-quality merits, is the deep reason we're betting on this substrate, whose value increases monotonically with time, while every other substrate's value plateaus.
The next essay turns to contradiction directly. We've hinted at the contradiction engine throughout these first two essays. In essay three, we open it up: what it is, how it detects, how it classifies, how it routes, and what it does when three experts walk into a graph and refuse to agree.
END OF ESSAY 02 · CONTINUE TO ESSAY 03 →