Graphiti: the temporal knowledge graph that gives agents a memory of time

Graphiti: the temporal knowledge graph that gives agents a memory of time

Ask a knowledge graph "what is true right now?" and most of them freeze. They were built at index time, they hold a snapshot, and the moment a fact changes they quietly go stale. That is fine for a document corpus that never edits itself. It is disqualifying for an AI agent that has to remember that "Kendra loved Adidas shoes as of March 2026" — and then remember, in April, that she switched to Nike. Graphiti is the framework built for exactly that problem: temporal knowledge graphs where every fact carries a validity window and the graph evolves as the world does.

This article digs into what a context graph actually is, why temporal tracking is the difference between a demo and a durable agent, how Graphiti compares to static GraphRAG, and — most usefully — a working Docker Compose stack you can run tonight to see it for yourself.


What a context graph is, and why "temporal" is the whole point

A context graph is a temporal graph of entities, relationships, and facts. The canonical example from the Graphiti docs is "Kendra loves Adidas shoes (as of March 2026)." Unlike a traditional knowledge graph, each fact in a context graph has a validity window: when it became true, and when (if ever) it was superseded. Entities evolve over time with updated summaries. And everything traces back to episodes — the raw data that produced it.

The four components that make up a context graph (from the Graphiti README):

The word that does the heavy lifting is temporal. When information changes, Graphiti invalidates the old fact rather than deleting it. You can query what is true now, or what was true at any point in the past. That single design decision is what makes the graph a memory instead of a cache.

Why static GraphRAG is not enough for agents

The GraphRAG approach that Microsoft popularized (arXiv:2404.16130) is brilliant at one thing: turning a large static corpus into a queryable graph with community summaries, so you can ask whole-corpus "sensemaking" questions. But it is batch-oriented and static. You index once, you summarize, and the graph reflects the corpus as it was at index time. The Graphiti README draws the contrast directly:

GraphRAG is for static document summarization; Graphiti is for dynamic, evolving temporal knowledge graphs. GraphRAG is batch-oriented; Graphiti is continuous and incremental. GraphRAG tracks basic timestamps; Graphiti does explicit bi-temporal tracking with automatic fact invalidation.

For an agent that operates on evolving, real-world data — a user's changing preferences, a product's changing status, a policy that gets revised — a static graph is a liability. The agent reasons over a snapshot that is already wrong. Graphiti's answer is incremental graph construction: new data integrates immediately without batch recomputation, and the graph evolves in real time as episodes are ingested.

The retrieval model: hybrid, not just semantic

Graphiti's retrieval is hybrid — it combines semantic embeddings, keyword (BM25), and graph traversal. The README is explicit that this is a deliberate choice to avoid reliance on LLM summarization at query time:

Hybrid Retrieval: combines semantic embeddings, keyword (BM25), and graph traversal for low-latency, high-precision queries without reliance on LLM summarization.

The practical payoff is latency. The README's comparison table puts GraphRAG query latency at "seconds to tens of seconds" (because it runs an LLM over community summaries at query time) versus Graphiti's "typically sub-second." For an agent that needs to look something up mid-conversation, that difference is the difference between a snappy assistant and a pause.

Prescribed and learned ontology

A subtle but important capability: Graphiti supports both prescribed and learned ontology. You can define entity and edge types up front via Pydantic models (prescribed), or let structure emerge from your data (learned). The README's framing: "Start simple, evolve as patterns appear." This is the pragmatic middle path between a rigid schema and a free-for-all — you get type safety where you need it and flexibility where you don't.

The stack: bring your own graph database

Graphiti is the open-source framework at the core of Zep's context infrastructure, but it is deliberately self-hosted and bring-your-own-database. The README lists the supported backends:

Installation is a single line: pip install graphiti-core (or uv add graphiti-core), with extras for each backend — graphiti-core[falkordb], graphiti-core[neptune], and optional LLM-provider extras like graphiti-core[anthropic] or graphiti-core[google-genai].

The LLM requirement: structured output matters

Graphiti depends on LLM services that reliably honor structured output (JSON schema) for entity and edge extraction and deduplication. The README is blunt about this:

Graphiti works best with LLM services that support Structured Output (such as OpenAI, Anthropic, and Gemini). Using other services may result in incorrect output schemas and ingestion failures. This is particularly problematic when using smaller models.

It defaults to OpenAI for both LLM inference and embedding, so an OPENAI_API_KEY is the fastest path. But it also supports OpenAI-compatible endpoints — DeepSeek, Together, OpenRouter, and local servers like Ollama, vLLM, and llama.cpp — via OpenAIGenericClient. The practical rule from the docs: use the most capable model you can run, because very small models frequently emit JSON that doesn't match the schema, which surfaces as extraction failures.

A working Docker Compose stack

The fastest way to see Graphiti in action is the official FastAPI server image, zepai/graphiti:latest, wired to a Neo4j container. The official server README gives the pattern, and the repo's own docker-compose.yml shows the healthchecks and profiles. Here is a self-contained, downloadable stack that brings up Neo4j plus the Graphiti API with a healthcheck and persistent storage:

# docker-compose.yml — Graphiti temporal knowledge graph stack
# Usage:  export OPENAI_API_KEY=sk-...  &&  docker compose up -d
# API:    http://localhost:8000/docs   (Swagger UI)
# Neo4j:  http://localhost:7474        (browser, neo4j/password)

services:
  graphiti:
    image: zepai/graphiti:latest
    container_name: graphiti
    ports:
      - "8000:8000"
    depends_on:
      neo4j:
        condition: service_healthy
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - NEO4J_URI=bolt://neo4j:${NEO4J_PORT:-7687}
      - NEO4J_USER=${NEO4J_USER:-neo4j}
      - NEO4J_PASSWORD=${NEO4J_PASSWORD:-password}
      - PORT=8000
      - db_backend=neo4j
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthcheck')"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  neo4j:
    image: neo4j:5.26.2
    container_name: graphiti-neo4j
    ports:
      - "7474:7474"          # HTTP browser
      - "${NEO4J_PORT:-7687}:${NEO4J_PORT:-7687}"  # Bolt
    volumes:
      - neo4j_data:/data
    environment:
      - NEO4J_AUTH=${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD:-password}
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:${NEO4J_PORT:-7474} || exit 1"]
      interval: 1s
      timeout: 10s
      retries: 10
      start_period: 3s
    restart: unless-stopped

volumes:
  neo4j_data:

The healthchecks matter: the API container waits for Neo4j to be healthy before it starts, and the API exposes its own /healthcheck endpoint so you can confirm the whole stack is up. The neo4j_data volume persists your graph across container restarts.

The quickstart, in code

Once the stack is up, the core loop is: connect, add episodes, search. The official quickstart (examples/quickstart) shows the shape. You initialize Graphiti against Neo4j, add episodes as text or structured JSON, and the framework extracts entities and relationships automatically:

from graphiti_core import Graphiti
from graphiti_core.nodes import EpisodeType

# Connect to Neo4j and set up Graphiti indices
graphiti = Graphiti("bolt://localhost:7687", "neo4j", "password")

# Add an episode (text) — Graphiti extracts entities + relationships
await graphiti.add_episode(
    name="Freakonomics Radio 0",
    episode_body="Kamala Harris is the Attorney General of California.",
    source=EpisodeType.text,
    source_description="podcast transcript",
)

# Hybrid search: semantic + BM25 + graph traversal
results = await graphiti.search("Who was the California Attorney General?")
for r in results:
    print(r.fact, r.valid_at, r.invalid_at)

The search results carry valid_at and invalid_at — the temporal validity window. That is the whole thesis in one line: the answer comes back with when it was true, not just what it was.

Zep vs Graphiti: when to use which

Graphiti is the open-source core; Zep is the managed platform built on top of it. The README is honest about the split:

The underlying architecture is documented in the Zep paper, "Zep: A Temporal Knowledge Graph Architecture" (arXiv:2501.13956), which is worth reading if you want the full design rationale behind bi-temporal tracking and fact invalidation.

The bottom line

Static knowledge graphs are a snapshot; agents need a memory. Graphiti's contribution is to make "what was true, when" a first-class query — facts carry validity windows, old facts are invalidated rather than deleted, and everything traces back to the raw episodes that produced it. For any agent that operates on changing real-world data, that is the difference between reasoning over a live memory and reasoning over a stale cache. The Docker Compose stack above gets you a running temporal knowledge graph in minutes — and the quickstart shows how little code it takes to start feeding it.


Sources