← labs | 04 | zep
lab 04 | ~5 min

What was true on Tuesday?

Most memory layers store a fact and call it done. Zep stores when each fact became true (valid_at) and when it stopped being true (invalid_at). The graph owns temporal truth. When this finishes you have a fact added, superseded, and queryable as of a past timestamp.

the temporal model in one paragraph

Zep is built on Graphiti, a temporally-aware knowledge graph. Every entity-relation edge has two timestamps. valid_at is when the fact became true in the world. invalid_at is when the fact stopped being true (null = still valid). When you tell Zep "I switched to pnpm," it doesn't overwrite an older "I use npm" edge; it sets that edge's invalid_at to now and creates a new edge with valid_at = now. Queries can target "as of last Tuesday" by filtering edges where Tuesday falls between the two timestamps.

"Zep tracks memory in temporal edges where the graph owns the truth about when a fact was valid, per valid_at and invalid_at in graphiti_core/edges.py."
arXiv:2501.13956 + graphiti-ai/graphiti, EntityEdge model
step 1

Sign up + install.

Free Zep Cloud account at getzep.com. Grab an API key from the dashboard. Then:

install
pip install zep-cloud

Set the env var:

bash | zsh
export ZEP_API_KEY="your-zep-key"
powershell
$env:ZEP_API_KEY = "your-zep-key"
cmd.exe
set ZEP_API_KEY=your-zep-key
step 2

Plant a fact. Supersede it.

Add a user, open a session, push two messages a day apart. The second message will supersede the first. Zep extracts the entity-relation edges asynchronously (so we sleep briefly to let the graph settle before querying).

zep_demo.py
import os
import sys
import time
import uuid
sys.stdout.reconfigure(encoding="utf-8")

from zep_cloud.client import Zep

client = Zep(api_key=os.environ["ZEP_API_KEY"])

user_id = f"ray-{uuid.uuid4().hex[:6]}"
session_id = f"sess-{uuid.uuid4().hex[:6]}"

client.user.add(user_id=user_id, first_name="Rayyan")
client.memory.add_session(session_id=session_id, user_id=user_id)

# week 1: npm
client.memory.add(
    session_id=session_id,
    messages=[
        {
            "role": "Rayyan",
            "role_type": "user",
            "content": "I use npm for everything.",
        },
    ],
)

# week 2: switched
time.sleep(3)
client.memory.add(
    session_id=session_id,
    messages=[
        {
            "role": "Rayyan",
            "role_type": "user",
            "content": "I just switched from npm to pnpm.",
        },
    ],
)

print(f"user_id={user_id} session_id={session_id}")
print("graph extraction is async; sleeping 8s before query...")
time.sleep(8)
step 3

Query the graph.

Append to the same file. We ask for the user's current state, then dump the raw edges so you can see valid_at and invalid_at in the JSON.

zep_demo.py (continued)
# natural-language query
results = client.graph.search(
    user_id=user_id,
    query="what package manager does the user use?",
    limit=5,
)

print("\n--- SEARCH RESULTS ---")
for r in results.edges or []:
    print(f"  fact: {r.fact}")
    print(f"    valid_at:   {r.valid_at}")
    print(f"    invalid_at: {r.invalid_at}")
    print()

# dump every edge for this user, to see supersession.
# the listing API has moved across SDK versions -- try the current name first,
# fall back to the older one. either way we get back an iterable of edges.
print("--- ALL EDGES FOR USER ---")
try:
    all_edges = client.graph.edge.get_by_user_id(user_id=user_id)
except AttributeError:
    try:
        all_edges = client.graph.edge.list(user_id=user_id)
    except AttributeError:
        all_edges = client.graph.list_edges(user_id=user_id)
for e in all_edges:
    state = "CURRENT" if e.invalid_at is None else "SUPERSEDED"
    print(f"  [{state}] {e.fact}")
    print(f"     valid_at:   {e.valid_at}")
    print(f"     invalid_at: {e.invalid_at}")

Run:

terminal
python zep_demo.py
expected output (approximate)
user_id=ray-a4f2c1 session_id=sess-9b1e08
graph extraction is async; sleeping 8s before query...

--- SEARCH RESULTS ---
  fact: Rayyan uses pnpm
    valid_at:   2026-05-11T19:24:31Z
    invalid_at: None

--- ALL EDGES FOR USER ---
  [SUPERSEDED] Rayyan uses npm
     valid_at:   2026-05-11T19:24:28Z
     invalid_at: 2026-05-11T19:24:31Z
  [CURRENT]    Rayyan uses pnpm
     valid_at:   2026-05-11T19:24:31Z
     invalid_at: None

why this matters

A vector store would have both facts at high similarity to "what package manager?" and the agent would have to guess which is current. Zep records that the npm fact became invalid the moment the pnpm fact was asserted. Time is first-class.

To query as of a past timestamp, pass center_node_uuid and limit to graph.search with a filter, or hit the edges directly and filter where valid_at <= T < invalid_at. The Graphiti repo has the full filter shape.

troubleshooting

Empty edges list after 8s sleep. Graphiti extraction is genuinely async on the free tier. Bump the sleep to 15-20s and retry. If still empty, the user-id wasn't created cleanly; run with a new random suffix.

ImportError on `from zep_cloud.client import Zep`. Some versions of the SDK use from zep_cloud import Zep. Either import works on current builds; try the shorter one if the long one fails.

"role_type required" or schema rejection. The cloud SDK is strict about the message shape. Both role (the display name) and role_type (one of "user", "assistant", "system", "tool") must be set on every message.