Start with a simple memory problem. A user worked on Team A last year and moved to Team B this year. If the agent keeps only the latest fact, it cannot answer where the user worked last year. If it keeps both facts as equally current, it can assign today's tasks to the wrong team. Graphiti's temporal context graph is built for this shape of memory: facts change, but history still matters.
This chapter follows that example all the way through: episode1 says the user was on Team A, episode2 says the user moved to Team B. The goal is not merely to retrieve both sentences. The old relationship should leave the current view while remaining available for historical questions.
Reading contract.
By the end, you should be able to explain why Graphiti calls raw inputs episodes, what
valid_at, invalid_at, and expired_at mean on
EntityEdge, and why hybrid retrieval combines semantic search, keyword search, and graph traversal.
First replay the “Team A to Team B” fact as a lifecycle. A good way to read Graphiti is through fact ownership: raw text lands as an episode, becomes entities and relationships, and then time fields decide which relationship belongs in the current view. Once that chain is clear, the later fields and search strategies are much easier to read.
| Stage | Shape Of The Fact | Who Uses It Later |
|---|---|---|
| Raw input | episode1 / episode2, with source, content, and valid_at. | Provides provenance and the fact's reference time. |
| Graph landing | EpisodicNode, entity nodes, and episode edges. |
Connects the raw stream to objects in the graph. |
| Relationship resolution | EntityEdge stores fact text, embedding, episodes, and time fields. |
Serves similarity search, provenance, and temporal judgment together. |
| New fact arrives | Old edge enters invalidated_edges; new edge enters new_edges. |
The current view sees Team B, while historical questions can still reach Team A. |
Evidence boundary. This article uses only the public getzep/graphiti README and source code. The README describes Graphiti as the open-source temporal context graph engine at the core of Zep's context infrastructure. Zep's managed scale, governance, and low-latency claims are treated as public positioning, not inferred private implementation.
1. Why Memory Needs a Temporal Context Graph
1.1 Vector Search Finds Similar Sentences; It Does Not Know When They Were True
Graphiti's README gives a precise frame: it builds and queries temporal context graphs for AI agents. Those graphs track how facts change over time, maintain provenance to source data, and support both prescribed and learned ontology. The README also contrasts it with traditional RAG: Graphiti continuously integrates interactions, structured and unstructured data, and external information, with incremental updates and historical queries instead of complete graph recomputation. That framing lives in the README introduction.
A vector-only memory can find both “the user works on team A” and “the user works on team B.” It does not naturally know that one may be historical and the other current. Overwriting the older fact breaks questions like “where did they work last year.” Keeping both without a validity window can pollute today's task routing. Graphiti's graph is not decoration on top of search. It puts fact, entity, source, and temporal validity in the same query structure.
1.2 The Context Graph Is A Fact Ledger, Not A Static Knowledge-Graph Screenshot
Concretely, the context graph has four main components: entities, facts / relationships, episodes, and custom types. The README table describes facts / relationships as triplets with temporal validity windows. Episodes are ingested raw data, the ground-truth stream that derived facts can trace back to. That makes the graph more than a decorative relationship view. It becomes a fact ledger with time and provenance.
| Component | Meaning in Graphiti | Memory question |
|---|---|---|
| Episode | Raw input stream with source, content, and valid_at. | Where did this fact come from? |
| Entity | People, projects, documents, places, and concepts. | Which object does the memory refer to? |
| Fact edge | A relationship with fact text, embedding, source episodes, and time fields. | When was this relationship true, and is it still true? |
| Current view | A query-time subgraph assembled for the current turn. | Which part should the model see now? |
2. Episodes Are Evidence, Not Ordinary Summaries
2.1 Separate Write Time From Fact Time
Graphiti lands inputs as EpisodicNode. The source model stores source,
source_description, content, valid_at, entity_edges,
and episode_metadata. The valid_at field is described as the creation time of
the original document. When the node is saved, content, entity_edges,
created_at, and valid_at are written into the graph. The implementation is in
EpisodicNode.
class EpisodicNode(Node):
source: EpisodeType = Field(description="source type")
source_description: str = Field(description="description of the data source")
content: str = Field(description="raw episode data")
valid_at: datetime = Field(description="datetime of when the original document was created")
entity_edges: list[str] = Field(default_factory=list)
episode_args = {
"content": self.content,
"entity_edges": self.entity_edges,
"created_at": self.created_at,
"valid_at": self.valid_at,
"source": self.source.value,
}
The distinction matters. created_at is when the record entered the system.
valid_at is the reference time of the original information. If a user imports last year's
meeting notes today, the write time is today, but the facts may belong to last year. Historical questions
need that difference.
A user imports last year's meeting notes today:
created_at = today, when the system observed the document
valid_at = last year, when the fact in the document applied
Question: "Which team is current?"
use the currently valid fact edge
Question: "Which team was it last year?"
read the historical validity window
2.2 Episode Edges Let Facts Return To Their Raw Input
Episodes are connected to entities through EpisodicEdge. Its
save()
path writes episode_uuid, entity_uuid, uuid, group_id,
and created_at. In plain terms, this records which entities were mentioned by a raw input.
Fact-level provenance is built on top of those links.
Provenance is not decoration in long-term memory. It is the correction path. When a later user or process challenges a fact, the system needs to know which conversation, document, or external source produced the relationship. Otherwise the graph only says that two nodes are related, without explaining why the relation appeared, when it appeared, or whether it should be withdrawn.
3. Fact Edges Carry Facts, Provenance, and Validity
3.1 A Relationship Edge Carries Text, Embedding, Source, And Time
The central memory object is EntityEdge. Its fields are direct:
name is the relationship name, fact stores the fact text,
fact_embedding supports similarity search, and episodes stores episode ids that
reference the edge. The time fields are the key: valid_at says when the fact became true,
invalid_at says when it stopped being true, and expired_at says when the edge
was invalidated by the system. These fields and their save path appear in
EntityEdge.
class EntityEdge(Edge):
name: str = Field(description="name of the edge, relation name")
fact: str = Field(description="fact representing the edge and nodes that it connects")
fact_embedding: list[float] | None = Field(default=None)
episodes: list[str] = Field(default=[])
expired_at: datetime | None = Field(default=None)
valid_at: datetime | None = Field(default=None)
invalid_at: datetime | None = Field(default=None)
reference_time: datetime | None = Field(default=None)
Each field owns a different part of the memory problem. fact is readable evidence for the model.
fact_embedding supports recall. episodes preserves source linkage. valid_at
and invalid_at answer temporal questions, while expired_at records when the system
invalidated an old edge. Graphiti is not keeping a graph next to text memory; it turns the edge itself into a
retrieval object, a fact object, and a temporal object.
3.2 Invalidation Is Not Deletion
With that model, new information does not have to erase old relationships. Graphiti's edge resolution
path returns resolved_edges, invalidated_edges, and new_edges.
The docstring for
_extract_and_resolve_edges()
says invalidated_edges are old edges invalidated by new information, while
new_edges are genuinely new non-duplicate edges. The write path is therefore neither a plain
append-only log nor a blind overwrite. It explicitly records that one fact superseded another.
resolved_edges, invalidated_edges, new_edges = await resolve_extracted_edges(
self.clients,
edges,
primary_episode,
nodes,
edge_types or {},
edge_type_map,
)
return resolved_edges, invalidated_edges, new_edges
Shape-level example: team fact after an update
edgeA:
fact = "User belongs to Team A"
episodes = ["episode1"]
valid_at = 2025-03-01
invalid_at = 2026-01-10
expired_at = system time when the edge was marked invalid
edgeB:
fact = "User belongs to Team B"
episodes = ["episode2"]
valid_at = 2026-01-10
invalid_at = null
current-time question: the non-invalidated edgeB enters the answer candidates
time-qualified historical question: follow edgeA -> episode1 back to Team A
In the team example, episode1 creates an edge that says the user is a member of Team A. When episode2 arrives,
resolution can put that older edge into invalidated_edges and create a new Team B edge in
new_edges. Current tasks see the still-valid Team B relationship. Historical questions can still
follow the old edge and episode1 back to Team A. valid_at is the business time when the fact
claims to start being true; invalid_at is the business time when a later fact supersedes it;
expired_at is the system time when the invalidation marker was written.
This is useful to compare with Mem0 v3. Mem0 v3 preserves append history at the memory-record layer and pushes time interpretation, ranking, and decay into retrieval and context assembly. Graphiti puts temporal windows directly on fact edges, so the graph itself knows whether a fact is current and whether it was true before. Both avoid deleting history; they put the responsibility in different layers.
4. Retrieval Assembles a Current Subgraph
4.1 Search Fans Out Because There Is More Than One Object Type
Once facts live in the graph, retrieval cannot be just one text similarity score. The README describes
hybrid retrieval as a combination of semantic embeddings, keyword / BM25, and graph traversal. The source
config mirrors that split. In
search_config.py,
edge and node search support cosine similarity, BM25, and BFS. Rerankers include RRF, node distance,
episode mentions, MMR, and cross encoder.
class EdgeSearchMethod(Enum):
cosine_similarity = "cosine_similarity"
bm25 = "bm25"
bfs = "breadth_first_search"
class EdgeReranker(Enum):
rrf = "reciprocal_rank_fusion"
node_distance = "node_distance"
episode_mentions = "episode_mentions"
mmr = "mmr"
cross_encoder = "cross_encoder"
This is more than “vector plus BM25.” A query may first hit a fact edge, or it may hit an entity and then walk to nearby relationships. It may also need an episode to explain provenance. The different search methods feed one context-assembly step, so Graphiti retrieval is closer to “find a candidate subgraph” than “return the ten most similar chunks.”
4.2 The Current Answer Needs A Time-Filtered, Relationship-Aware View
During execution, if the config needs cosine similarity or MMR,
search()
creates an embedding for the query. A single search result can return edges, nodes, episodes, and
communities. The retrieval target is a set of related, sourced, time-aware objects that can form the
model's current view, rather than a single text chunk with the highest similarity score.
| Query stage | What Graphiti is looking for | Effect in the team example |
|---|---|---|
| Semantic / BM25 | Relevant fact edges, nodes, and episodes. | Terms such as team, Team A, and Team B can all enter the candidate set. |
| BFS / graph traversal | Relationships adjacent to matched entities. | A hit on the user node can still walk to team relationships. |
| RRF / MMR / cross encoder | Merge and reorder candidates from multiple routes. | Reduces bias from relying on one scoring path. |
| Temporal filtering and context assembly | Select valid edges and provenance for the requested time point. | Current questions see Team B; historical questions can recover Team A. |
This also explains the Graphiti / Zep relationship. The README says Graphiti is the open-source temporal context graph engine at the core of Zep's context infrastructure. Zep manages context graphs at scale and provides governed, low-latency retrieval and assembly for production agent deployments. For source readers, Graphiti is the self-hostable graph memory kernel; Zep is the production context platform around that idea.
5. Back on the Agent Memory Map
5.1 Graphiti's Answer Is That Memory Belongs To The Temporal Graph
The Mem0, Letta, and Graphiti boundaries are now easier to name. Mem0 centers on a memory service: how to extract, write, retrieve, and rank memories from conversations. Letta centers on agent state: how memory blocks, messages, tools, files, and system prompts reconstruct a durable agent. Graphiti centers on the temporal graph: how relationships, validity windows, and provenance become queryable structure.
| Project | Memory owner | Core engineering question |
|---|---|---|
| Mem0 | External memory layer | How to extract and deduplicate on write, then rank, interpret time, and control noise on read. |
| Letta | Stateful agent | How long-term blocks, message windows, tools, and file views enter model-visible state. |
| Graphiti | Temporal context graph | How to preserve history, provenance, and a current queryable subgraph when facts change. |
The next chapter, LangMem / LangGraph, moves to another boundary. If memory is embedded inside the workflow runtime, should writes and consolidation happen in the agent's current hot path, or in a background manager? That question shifts long-term memory from what to store and how to search toward when memory changes and which execution path is allowed to change it.
Sources
- Graphiti README: temporal context graph positioning
- Graphiti README: context graph components
- Graphiti README: Graphiti and Zep
- Graphiti README: Temporal Fact Management and Hybrid Retrieval
- Graphiti
EpisodicNode - Graphiti
EpisodicEdge - Graphiti
EntityEdge - Graphiti edge extraction and invalidation path
- Graphiti search config
- Graphiti search execution