Skip to content
xmemoryxmemory Website

xmemory vs Zep

Of the memory products we get asked about, Zep is the closest to us in spirit. It has an ontology. Facts carry time. Contradictions are handled rather than ignored. The short version of this page: Zep’s types decide how extracted facts are classified and matched; XMD types decide how extracted values are shaped and resolved. Those are different constraints, and the difference shows up the moment code, rather than a model, has to consume the memory.

Documentation reviewed 23 October 2026: Zep v4 concepts, customizing graph structure, facts, querying a graph, Zep vs Graphiti and the Zep paper; on our side How xmemory works, XMD and the API.

Zep ingests episodes (messages, documents, JSON) into a per-user or shared context graph: nodes are entities, edges are facts written as sentences with a relationship name, and every fact carries bi-temporal timestamps (valid_at, invalid_at, plus when Zep learned each). When a new episode contradicts an old fact, the old edge is invalidated rather than deleted, so the graph can answer “what was true in March”. Retrieval is hybrid (semantic, BM25, graph traversal, rerankers); the usual output is a context block, prompt-ready text listing relevant facts with their date ranges, and nodes and edges can also be listed and fetched as records.

You can shape the graph with custom entity and edge types: up to ten typed attributes per type, each with a description, identity_properties that decide which node a mention matches, protected attributes that keep their first value, and a strict_ontology switch that drops entities and edges outside the types you configured. The engine underneath, Graphiti, is open source on Neo4j, FalkorDB or Neptune; Zep is the managed service on top.

That is a serious design, and for conversational agents that need to know how a relationship changed over time it works well.

What the types constrain. Zep’s constraints are about membership and identity: which type a fact belongs to, which node it attaches to, whether an unmatched fact is kept or dropped. Inside a type, attributes are text or boolean with a description; there are no enumerated values, no required-field semantics, and the docs say classification favours recall. XMD’s constraints are about values: enums bound what a field may hold and extraction picks from them, keys decide which record a mention updates, relation keys fix cardinality, and a required field the text cannot ground stays empty rather than being invented (the XMD reference is precise about what required does and does not mean). When downstream code reads a populated status, it reads one of the values you allowed.

Where state lives, and what you can do with it. Zep can hold an account’s status as an attribute on an entity; by default it holds it as a fact, “account Emily0e62 has a suspended status due to payment failure (2024-11-14 - present)”. Either way the ways to get it back are search, traversal, list and fetch, and an experimental natural-language graph.query that runs a retrieval loop. None of those is an aggregate. In xmemory status, reason and suspended_on are columns in a store with SQL underneath, so “how many accounts were suspended this week, and which ones” is a query over all matching stored records, rather than a ranked selection of facts. That is the argument of Schema as the Core of Reliability, and it is the difference the benchmark below is built to test.

How a correction lands. Zep’s invalidation is a judgement made at ingestion: the engine searches for related edges and decides whether the new fact supersedes one, then stamps invalid_at. xmemory looks up the record whose primary key matches, writes the new value, and keeps the previous value as lineage. No similarity search is involved, and you express identity and update rules in the schema. The Why page walks through the “Amy is no longer Kevin’s manager” case for both approaches.

Postgres, not a graph database. This one is a deployment preference rather than a capability. Graphiti needs Neo4j, FalkorDB or Neptune; Zep Cloud removes that by hosting it. xmemory keeps relations as schema objects over Postgres, so self-hosting means a database your team already runs, and the same API serves relations, records and queries.

The measurements. Zep’s paper reports 94.8% on Deep Memory Retrieval (MemGPT: 93.4%) and up to 18.5% better accuracy on LongMemEval, both recall-style benchmarks. On our four-domain end-to-end benchmark, which tests updates, deletions, relation changes, aggregation and negative queries, Zep scored 80.16 F1 against xmemory’s 97.10; on the Splitwise task, where every read needs arithmetic over stored facts, 25.7% against 95.2% (white paper, summarised in Chasing AI memory SOTA). We ran Zep Cloud through the zep-cloud Python SDK with edge-scoped search, the cross-encoder reranker and ten facts per query, then the same synthesis prompt as every other baseline (setup in section 8.3); the numbers measure that configuration, not everything v4 can be tuned to do. They do not prove why the gap exists. They do show that on the operations this benchmark isolates, the gap is large.

Choose Zep when the product is a timeline of what people said and how relationships shifted, the main consumer is a model reading a context block, and search plus fetch covers what code needs. Choose xmemory when code has to count, join, filter or act on memory, and you want the values it acts on bounded by a contract. The ontology you would write for Zep is a good first draft of the schema; the difference is what each system does with it.

What you would declare as entity and edge types in Zep maps onto XMD objects and relations, with one habit to change: anything you want to query lives in a typed field, and a relationship that carries state becomes an object with a key that expresses the rule you want.

xmd_version: v1
title: Org memory
description: People, where they work, and who they report to, kept as current state.
objects:
Person:
description: An individual named in the text. Not a team or a company.
fields:
name: { type: str, required: true }
title: { type: str, required: false }
primary_key: [name]
Organisation:
description: A company, team or department.
fields:
name: { type: str, required: true }
primary_key: [name]
ReportingLine:
description: >-
Who a person currently reports to. One record per report; a statement that
someone has a new manager updates that person's record.
fields:
report_name: { type: str, required: true }
manager_name: { type: str, required: true }
primary_key: [report_name]
relations:
works_at:
description: Link a Person to the Organisation that employs them.
objects:
person: { type: Person, on_delete: cascade }
organisation: { type: Organisation, on_delete: nullify }
keys:
one_employer: [person]
reports_to:
description: Connect a ReportingLine to the report and the manager it names.
objects:
line: { type: ReportingLine, on_delete: cascade }
report: { type: Person, on_delete: cascade }
manager: { type: Person, on_delete: cascade }
keys:
one_pair_per_line: [line]

Zep’s WORKS_AT edge is the works_at relation. Its “Amy manages Kevin (March - present)” fact is a ReportingLine record keyed on the report. “Kevin now reports to Priya” matches that key and rewrites manager_name; the engine keeps Amy as the previous value in lineage, and nothing else has to notice. The rule is in the key: one current manager per person. The record is deliberately narrow. A field such as since would need its own rule for what happens when the manager changes and no new date is given, and if the product needs each period as its own record (Kevin reports to Amy, then Priya, then Amy again), periods should get an identity and be keyed on that instead. That is the trade: Zep gives you a time range on every edge automatically, with the supersession decided at ingestion; xmemory gives you the exact rule you wrote, with history kept by the engine.

Why we think this matters, and for whom

xmemory works well for short-running agents, and it is at its best for repeated tasks, where each run adds to a body of knowledge the next run can use. Long-horizon agents deserve a special mention, because there is very little objective evidence yet about what memory design works for them. Our working belief, and the reason xmemory exists: an agent running a long, multi-step task does better on structured state than on accumulated context. A schema gives it an explicit feedback loop at write time (what was accepted, what was rejected, what changed), so memory is updated rather than bloated and the trajectory stays on course: fewer re-reads, faster steps, more reliable outcomes.

If that is your workload, try xmemory on it. We are building objective measurements for this claim. The Toloka evaluation is our first independent measurement of the benefits of structured memory; evaluating changing working state over long-horizon tasks is next, and we would welcome collaborators on it. Write to info@xmemory.ai.