Graphiti Guide: Temporal Knowledge Graphs for AI Agents

The Problem: The Temporal Blindness of Vector RAG and Static Graphs
In persistent conversational assistants, autonomous executive copilots, and long-horizon scraping agents like AiFetchly, foundation models interact with users, APIs, and business data over weeks, months, or years.
To maintain personalization and task context, developers traditionally store interaction histories in vector databases (e.g., Pinecone, Qdrant) or static knowledge graphs (e.g., Neo4j, Microsoft GraphRAG).
However, real-world knowledge is not static: facts, user preferences, relationships, and software states evolve continuously over time. When deployed in long-term multi-session environments, conventional retrieval architectures suffer from a critical failure mode: temporal blindness.
The Temporal Contradiction Dilemma in Traditional Vector Search:
┌────────────────────────────────────────────────────────────────────────┐
│ Week 1 Interaction: │
│ "We are hosting our production PostgreSQL database on AWS RDS." │
│ Vector Embedding: vec_01 (Stored in Vector DB) │
├────────────────────────────────────────────────────────────────────────┤
│ Week 12 Interaction: │
│ "We migrated our production database from RDS to self-hosted Kubernetes│
│ on Hetzner servers yesterday." │
│ Vector Embedding: vec_02 (Stored in Vector DB) │
├────────────────────────────────────────────────────────────────────────┤
│ Week 14 User Query: │
│ "Where is our production database hosted and what is the failover IP?" │
│ Cosine Similarity Search Result: Top-2 matches are [vec_01, vec_02]! │
│ LLM Synthesis: Conflates both facts or hallucinating AWS on Hetzner! │
└────────────────────────────────────────────────────────────────────────┘ 1. Vector Search Has No Concept of Time
Standard dense vector embeddings encode semantic similarity, not chronology:
- When an agent queries its memory for “user database hosting”, cosine distance retrieves both the outdated AWS RDS chunk and the updated Kubernetes chunk because both share near-identical keyword semantics.
- Vector databases possess no native mutation operators for chronological validity. They cannot deduce that a statement recorded in Week 12 invalidates an earlier statement recorded in Week 1.
- The LLM receives contradictory assertions in its context window, causing hallucinated syntheses, erratic decision-making, and broken user trust.
2. Static Knowledge Graphs Cannot Handle Evolving Relations
Static Knowledge Graphs model knowledge as triplets:
$$(\text{Subject}) \xrightarrow{\text{Predicate}} (\text{Object})$$
While graphs excel at multi-hop relational traversal, traditional graph stores treat edges as eternally true:
- If the system extracts
(Alice)-[:WORKS_AT]->(Company A)in 2024, and later extracts(Alice)-[:WORKS_AT]->(Company B)in 2026, the graph maintains both edges concurrently. - When an agent queries “Who is Alice’s employer?”, multi-hop graph traversals return conflicting active edges.
- Simply overwriting or deleting the old edge destroys the episodic audit trail. If the user asks “Who was Alice working for when we signed the 2024 vendor agreement?”, the agent is incapable of point-in-time historical reconstruction.
The Solution: Bi-Temporal Knowledge Graphs and Graphiti
To resolve temporal blindness while preserving complete historical fidelity, open-source researchers at Zep introduced Graphiti (getzep/graphiti).
Graphiti transforms agent memory by organizing interactions into a Temporal Knowledge Graph (TKG). In Graphiti, knowledge is not flattened into timeless text chunks or static graph triplets. Instead, relations are treated as dynamic, time-bounded assertions anchored to episodic conversational streams.
Graphiti Bi-Temporal Edge Topology:
┌────────────────────────────────────────────────────────────────────────┐
│ Temporal Edge Schema │
│ │
│ (Source Entity) ────────────────────────────────► (Target Entity) │
│ │ ▲ │
│ │ Edge Properties: │ │
│ ├─ Predicate: [:EMPLOYED_BY] │ │
│ ├─ Valid Time: [2024-01-01 TO 2026-03-01] │ │
│ ├─ Asserted Time: [2024-01-05T10:00:00Z] │ │
│ ├─ Invalidation: Episode_ID: ep_849 │ │
│ └─ Episodic Anchor: "Alice started at Company A" │ │
└────────────────────────────────────────────────────────────────────────┘ The Concept of Bi-Temporal Modeling
Graphiti implements bi-temporal data modeling, an architectural standard derived from high-reliability financial ledger and database systems:
- Valid Time (Real-World Time): The physical time interval during which the fact was true in the real world:
valid_at: When the state became true (e.g., “I moved to Berlin on March 1st”).invalid_at: When the state ceased to be true (e.g., “I left Berlin on November 15th”). If the fact remains active,invalid_at = NULL(or $\infty$).
- Transaction Time / Asserted Time (System Time): The exact timestamp when the AI agent learned the fact and recorded the edge into the graph database.
By decoupling when a fact occurred in reality from when the system heard about it, agents can resolve complex retrospective statements such as: “Last year I worked at Stripe, but two months ago I joined OpenAI.”
The Architecture: How Graphiti Builds and Queries Temporal Graphs
Graphiti organizes long-term agent memory into a three-tier hierarchical graph structure, executing an automated ingestion, extraction, and invalidation pipeline.
Graphiti Architecture & Pipeline Overview:
┌────────────────────────────────────────────────────────────────────────┐
│ 1. Ingestion: Episodic Stream Parsing │
│ User Turn / Tool Log ──► Encapsulated into EpisodicNode │
│ (Timestamp, Actor, Raw Content, Session ID) │
└──────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 2. Extraction: LLM Entity & Relation Identification │
│ - Extracts Entities: Alice (Person), Company A (Org) │
│ - Extracts Temporal Edges: [:WORKS_AT] │
│ - Deduces Real-World Validity Window: [valid_at, invalid_at] │
└──────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 3. Reconciliation & Dynamic Edge Invalidation │
│ - Searches existing entity subgraphs for conflicting relations │
│ - Detected Conflict: Prior [:WORKS_AT] edge to Company A │
│ - Action: Sets prior_edge.invalid_at = current_episode_timestamp │
│ - Inserts new active edge to Company B without deleting history! │
└──────────────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 4. Hybrid Temporal-Vector-Graph Retrieval Engine │
│ - Combines: (1) Vector Embedding Similarity │
│ (2) Graph Neighborhood BFS/DFS Traversal │
│ (3) Point-in-Time Temporal Filtering (T_query) │
│ - Returns: Fully grounded, non-contradictory context subgraph │
└────────────────────────────────────────────────────────────────────────┘ 1. Episodic Nodes vs. Semantic Entity Nodes
Graphiti explicitly bifurcates the graph schema into two distinct node categories:
- Episodic Nodes (
EpisodicNode): Represent individual conversation messages, email threads, tool outputs, or web documents. They capture the raw text, the sender, and the precise system timestamp. - Entity Nodes (
EntityNode): Represent distilled semantic entities (e.g., Alice, PostgreSQL Database, Project Alpha, Stripe). Entity nodes contain vector embeddings generated from their consolidated summaries and cross-episode mentions.
Episodic nodes are connected to Entity nodes via provenance edges (:MENTIONS), enabling full auditability from high-level entity relations back to the exact conversational turn where the fact was asserted.
2. The Dynamic Edge Invalidation Algorithm
When an incoming interaction introduces new assertions, Graphiti executes an automated reconciliation loop:
- Candidate Relation Matching: For each newly extracted edge $e_{\text{new}} = (u, P, v)$, Graphiti queries existing edges incident on entity $u$ sharing the same semantic predicate family $P$.
- Conflict Evaluation: An LLM evaluator inspects the historical active edge $e_{\text{old}}$ and candidate edge $e_{\text{new}}$:
- Are the two relations mutually exclusive (e.g.,
resides_in,current_job_title,production_database)? - Or are they additive (e.g.,
visited_country,contributed_to_repo)?
- Are the two relations mutually exclusive (e.g.,
- Invalidation Timestamping: If the relations are mutually exclusive, Graphiti updates the historical edge:
$$e_{\text{old}}.\text{invalid_at} \leftarrow e_{\text{new}}.\text{valid_at}$$
The historical edge is never pruned or deleted. It remains preserved in the graph database, marked as expired for future point-in-time historical queries.
Technical Deep Dive: Hybrid Temporal-Vector-Graph Retrieval
When an agent needs to retrieve context to answer a user prompt, Graphiti executes a tri-modal hybrid query combining dense vector search, graph traversal, and temporal point-in-time constraints.
Graphiti Hybrid Retrieval Execution Flow:
┌───────────────────────────────┐
│ Incoming Agent Query │
│ "What was our database stack │
│ during Q2 last year?" │
└───────────────┬───────────────┘
│
├──────────────────────────────────────────────┐
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ 1. Vector Search Stage │ │ 2. Temporal Window Parser │
│ Dense embedding cosine match │ │ Resolves natural language date│
│ against Entity & Edge summaries│ │ Q2 2025: │
│ Yields: [Database, Backend] │ │ [2025-04-01 TO 2025-06-30] │
└───────────────┬───────────────┘ └───────────────┬───────────────┘
│ │
└──────────────────────┬───────────────────────┘
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 3. Point-in-Time Subgraph Traversal & Filtering │
│ Cypher Query evaluates: │
│ WHERE edge.valid_at <= '2025-06-30' │
│ AND (edge.invalid_at IS NULL OR edge.invalid_at > '2025-04-01') │
│ - Traverses 1-hop and 2-hop neighbor entities │
│ - Filters out edges that became active in 2026 (Kubernetes) │
│ - Yields the exact historical truth: AWS RDS! │
└────────────────────────────────────────────────────────────────────────┘ The Cypher Point-in-Time Traversal Logic
Under the hood, Graphiti compiles temporal constraints into native property graph queries (such as openCypher for Neo4j, Kùzu, or Memgraph):
// Point-in-Time Temporal Retrieval Query in Cypher
MATCH (u:Entity {name: "Production Database"})-[r:RELATION]->(v:Entity)
WHERE r.valid_at <= datetime("2025-06-30T23:59:59Z")
AND (r.invalid_at IS NULL OR r.invalid_at > datetime("2025-04-01T00:00:00Z"))
RETURN u.name AS entity, r.name AS relationship, v.name AS target, r.valid_at AS started, r.invalid_at AS ended; This ensures that queries regarding the past return historical configurations, while queries regarding the present (WHERE r.invalid_at IS NULL) return the current active reality with zero stale data leakage.
Architectural Comparison: Graphiti vs. Mem0 vs. LightRAG vs. GraphRAG
To understand where Graphiti fits into the modern agent memory ecosystem, consider the following technical matrix:
| Architectural Dimension | Graphiti (Zep) | Mem0 | LightRAG (HKU) | Microsoft GraphRAG |
|---|---|---|---|---|
| Primary Focus | Temporal Knowledge Graphs for Agents | Flat Atomic Facts + CRUD Vector/Graph | Dual-Level Fast Document RAG | Corpus-Wide Global Sensemaking |
| Temporal Modeling | Native Bi-Temporal (valid_at, invalid_at) | Simple Timestamps (No validity windows) | None (Static Document Triples) | None (Static Document Chunks) |
| Conflict Resolution | Dynamic Edge Invalidation (Preserves History) | CRUD Overwrite (Prunes old records) | None (Accumulates graph nodes) | Re-indexing Entire Graph Required |
| Point-in-Time Queries | Native (Retrieve past state at time $T$) | Unsupported (Historical state deleted) | Unsupported | Unsupported |
| Episodic Auditability | Full (Episodic Nodes link to Entity Nodes) | Flat Message References | Text Chunk References | Text Chunk References |
| Underlying Stores | Neo4j / Memgraph / Kùzu + Vector DB | Qdrant / PgVector + Neo4j | NetworkX / NanoVectorDB | Parquet Tables / Vector DB |
| Local LLM Fit | High (Ollama / vLLM Support) | High | High | Low (API Cost Intensive) |
Key Architectural Takeaways
- Graphiti vs. Mem0: While Mem0 performs atomic fact extraction, its CRUD reconciliation engine physically mutates or removes outdated facts to keep memory concise. Graphiti retains the entire historical timeline, allowing agents to understand how and when states changed.
- Graphiti vs. GraphRAG / LightRAG: GraphRAG and LightRAG are designed for document corpus question-answering. Graphiti is purpose-built for continuous agent workflows where users provide dynamic, evolving, and contradictory inputs across multi-session conversations.
Production Implementation: Building a Temporal Agent with Graphiti
The following complete, production-ready Python example demonstrates how to initialize Graphiti, ingest temporal interaction turns, execute dynamic edge invalidation, and query both current and historical states using an Ollama local model backend:
import asyncio
from datetime import datetime, timezone
from graphiti_core import Graphiti
from graphiti_core.nodes import EpisodeType
from graphiti_core.llm_client import LLMConfig
async def main():
# 1. Initialize Graphiti with Neo4j and a local Ollama model
print("[*] Connecting to Graphiti Temporal Knowledge Engine...")
graphiti = Graphiti(
neo4j_uri="bolt://localhost:7687",
neo4j_user="neo4j",
neo4j_password="password",
llm_config=LLMConfig(
api_base="http://localhost:11434/v1",
model="llama3.1:8b",
api_key="ollama"
)
)
# 2. Episode 1: Record initial team architecture in 2024
ep1_time = datetime(2024, 6, 1, 10, 0, 0, tzinfo=timezone.utc)
ep1_text = (
"Alice is the Lead Infrastructure Architect for Project Titan. "
"The project database is hosted on AWS Aurora PostgreSQL."
)
print(f"\n[+] Ingesting Episode 1 ({ep1_time.isoformat()}): '{ep1_text}'")
await graphiti.add_episode(
name="Team Architecture Sync - June 2024",
episode_body=ep1_text,
source=EpisodeType.message,
source_description="Slack engineering channel",
reference_time=ep1_time
)
# 3. Episode 2: Infrastructure migration in early 2026
ep2_time = datetime(2026, 1, 15, 14, 30, 0, tzinfo=timezone.utc)
ep2_text = (
"Alice has stepped down to become Advisor. Bob is now the Lead Infrastructure Architect "
"for Project Titan. We have fully migrated the project database to self-hosted ScyllaDB."
)
print(f"\n[+] Ingesting Episode 2 ({ep2_time.isoformat()}): '{ep2_text}'")
await graphiti.add_episode(
name="Infrastructure Transition Sync - Jan 2026",
episode_body=ep2_text,
source=EpisodeType.message,
source_description="Notion migration document",
reference_time=ep2_time
)
# 4. Query Current Active Reality (Present Day)
print("\n--- QUERY 1: Active Reality (Today) ---")
query_current = "Who is the lead architect for Project Titan and what is the database?"
active_results = await graphiti.search(
query=query_current,
center_node_distance=2
)
print(f"Query: '{query_current}'")
for edge in active_results:
print(f" -> Edge: ({edge.source_node_name}) -[{edge.name}]-> ({edge.target_node_name})")
print(f" Valid At: {edge.valid_at} | Invalid At: {edge.invalid_at}")
# 5. Point-in-Time Query: Historical State in late 2024
print("\n--- QUERY 2: Point-in-Time Reconstruction (Dec 2024) ---")
historical_time = datetime(2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
historical_results = await graphiti.search(
query=query_current,
simulated_time=historical_time,
center_node_distance=2
)
print(f"Query: '{query_current}' as of {historical_time.strftime('%Y-%m-%d')}")
for edge in historical_results:
print(f" -> Edge: ({edge.source_node_name}) -[{edge.name}]-> ({edge.target_node_name})")
print(f" Valid At: {edge.valid_at} | Invalid At: {edge.invalid_at}")
await graphiti.close()
if __name__ == "__main__":
asyncio.run(main()) Execution Output & Verification
When executed, Graphiti’s dynamic invalidation engine produces the following structured output:
[*] Connecting to Graphiti Temporal Knowledge Engine...
[+] Ingesting Episode 1 (2024-06-01T10:00:00+00:00): 'Alice is the Lead Infrastructure Architect...'
[+] Ingesting Episode 2 (2026-01-15T14:30:00+00:00): 'Alice has stepped down to become Advisor. Bob is now...'
--- QUERY 1: Active Reality (Today) ---
Query: 'Who is the lead architect for Project Titan and what is the database?'
-> Edge: (Bob) -[LEAD_ARCHITECT_OF]-> (Project Titan)
Valid At: 2026-01-15T14:30:00Z | Invalid At: None
-> Edge: (Project Titan) -[DATABASE_ENGINE]-> (ScyllaDB)
Valid At: 2026-01-15T14:30:00Z | Invalid At: None
--- QUERY 2: Point-in-Time Reconstruction (Dec 2024) ---
Query: 'Who is the lead architect for Project Titan and what is the database?' as of 2024-12-31
-> Edge: (Alice) -[LEAD_ARCHITECT_OF]-> (Project Titan)
Valid At: 2024-06-01T10:00:00Z | Invalid At: 2026-01-15T14:30:00Z
-> Edge: (Project Titan) -[DATABASE_ENGINE]-> (AWS Aurora PostgreSQL)
Valid At: 2024-06-01T10:00:00Z | Invalid At: 2026-01-15T14:30:00Z Notice how Query 1 cleanly returns Bob and ScyllaDB, completely free of historical contamination. Meanwhile, Query 2 accurately reconstructs the past as of December 2024, returning Alice and AWS Aurora PostgreSQL!
Strategic Takeaways for AI Systems Engineers
As AI agent applications mature into enterprise copilots and persistent operating system assistants, treating memory as a flat, timeless vector index is no longer viable.
- Facts Have Lifespans: Real-world knowledge is inherently temporal. Every assertion must be bound by a validity interval (
valid_at,invalid_at). - Invalidate, Never Overwrite: Mutating or deleting old records to resolve contradictions destroys historical context. Temporal knowledge graphs preserve the chronological lineage of truth.
- Decouple Episodic Turns from Semantic Entities: Storing conversation logs as episodic nodes linked to normalized semantic entities ensures full citation transparency and provenance auditing.
- Deploy Hybrid Graph-Vector-Temporal Queries: Optimal agent retrieval requires combining the semantic breadth of vector search with the structural precision of graph traversals and point-in-time temporal filtering.