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.
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.
Free Zep Cloud account at getzep.com. Grab an API key from the dashboard. Then:
pip install zep-cloud
Set the env var:
export ZEP_API_KEY="your-zep-key"
$env:ZEP_API_KEY = "your-zep-key"
set ZEP_API_KEY=your-zep-key
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).
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)
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.
# 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:
python zep_demo.py
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
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.
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.