1. Executive Summary
Graphiti is a library for building an incrementally updated temporal
knowledge graph from episodes such as messages, events, or documents.
Its defining feature is bi-temporal memory: it distinguishes when an
episode or fact was true (valid_at/invalid_at)
from when the graph learned or expired it
(created_at/expired_at).
This is a genuinely different answer to agent memory. Graphiti does not primarily produce a bag of user facts. It preserves episodes, resolves entities, extracts typed relationships, invalidates contradicted edges, and searches across facts, entities, episodes, and communities. The graph remains queryable as history rather than overwriting the past with the latest summary.
Its strengths—temporal correctness, rich retrieval, custom ontology support—also create its risks. Ingestion is LLM-heavy, graph-database operations are operationally substantial, and entity/edge deduplication mistakes can restructure memory globally. It is best treated as a context-graph engine, not a drop-in truth store.
2. Mental Model
An episode is immutable evidence. Entities are normalized concepts mentioned by episodes. Entity edges are extracted claims, with temporal validity and links back to their source episodes. Communities and sagas provide higher-level organization and summaries.
episode(content, reference_time, group_id)
-> extract + resolve entity nodes
-> extract + dedupe entity edges
-> assign valid_at / invalid_at
-> invalidate contradictory older edges
-> connect episode to entities and episode chain
-> search edges + nodes + episodes + communities
Two clocks answer two different questions:
- “When was this believed/recorded?” uses
created_atandexpired_at. - “When was this true in the represented world?” uses
valid_atandinvalid_at.
That distinction makes backfilled and corrected history representable without pretending ingestion time equals event time.
Diagram source
%% caption: a contradicting fact closes the previous edge's validity interval instead of erasing it, so what was believed last March stays answerable — and nothing marks any claim verified or rejected
flowchart TB
Ep["Episode ingested"] --> Ext["Entity and edge extraction"]
Ext --> Res["Entity resolution<br/>against existing nodes"]
Res --> Edge[("Temporal relationship edge<br/>valid_at … invalid_at")]
New["A contradicting fact arrives"] --> Inv["Close the old interval:<br/>set invalid_at, keep the edge"]
Inv --> Edge
Edge --> Q["BM25 + cosine + BFS over<br/>edges, nodes, episodes, communities<br/>→ RRF / MMR / cross-encoder"]
Edge -.->|"history retained, so 'what did we<br/>believe last March' is answerable"| Hist["closed intervals<br/>queryable by date filter"]
Edge -.->|"and no claim is marked<br/>verified or rejected"| Gap["no trust state"]3. Architecture
Core areas:
graphiti_core/graphiti.py: public orchestrator for episodes, bulk ingestion, search, removal, sagas, and indices.graphiti_core/nodes.py: episodic, entity, community, and saga node types.graphiti_core/edges.py: episodic, entity, and community edge types plus persistence.graphiti_core/utils/maintenance/: extraction, resolution, dedupe, invalidation, and graph mutation.graphiti_core/search/: configurable multi-scope search and reranking recipes.graphiti_core/driver/: Neo4j, FalkorDB, Kuzu, and Amazon Neptune adapters.graphiti_core/llm_client/,embedder/, andcross_encoder/: model abstractions.mcp_server/andserver/: agent-facing service layers.
The design separates graph-domain objects from driver queries and LLM
clients. Search receives a GraphitiClients bundle, while
ingestion orchestration uses the same explicit clients. This supports
backend substitution, though not every backend has identical query or
index capabilities.
4. Essential Implementation Paths
- Single ingestion:
Graphiti.add_episode()ingraphiti_core/graphiti.py. - Bulk ingestion:
Graphiti.add_episode_bulk(). - Episode model:
EpisodicNodeingraphiti_core/nodes.py. - Entity/fact models:
EntityNodeandEntityEdgeinnodes.pyandedges.py. - Entity extraction/resolution:
utils/maintenance/node_operations.py. - Relationship extraction:
extract_edges()inutils/maintenance/edge_operations.py. - Contradiction and temporal invalidation: edge
resolution/invalidation functions in
edge_operations.py. - Search entrypoints:
Graphiti.search(),Graphiti.search_(), andsearch()insearch/search.py. - Search presets:
search/search_config_recipes.py. - Episode removal:
Graphiti.remove_episode(). - Database constraints: driver
build_indices_and_constraints()implementations.
5. Memory Data Model
EpisodicNode stores content, episode type, source
description, valid_at, created_at, and
group_id. Episodes can represent messages, text, or
JSON.
EntityNode stores a normalized name, summary,
labels/types, attributes, embedding, and scope. Optional
application-defined Pydantic models constrain entity attributes.
EntityEdge is the principal fact record:
- source and target entity UUIDs;
- relationship name and natural-language fact;
- source episode UUIDs;
- embedding and custom attributes;
valid_at,invalid_at,created_at, andexpired_at;group_idscope.
EpisodicEdge links an episode to mentioned entities.
Community nodes/edges group the entity graph. Saga nodes group episodes
and maintain summaries with both wall-clock and episode-time
watermarks.
6. Retrieval Mechanics
Graphiti searches four scopes concurrently: entity edges, entity nodes, episodes, and communities. Each scope can enable a combination of:
- BM25/full-text search;
- cosine similarity;
- graph BFS from explicit or discovered origins.
Results are fused and reranked with configurable recipes: RRF,
maximal marginal relevance, cross-encoder scoring, node distance, or
episode-mention signals. EDGE_HYBRID_SEARCH_RRF is the
default high-level recipe used by the convenient
Graphiti.search() path.
Filters support group IDs, dates, entity labels, and edge types. Search can also center on a node or begin BFS from supplied origins. This is more expressive than ordinary fact-vector recall, but query behavior depends heavily on choosing the right recipe and graph scope.
7. Write Mechanics
add_episode() saves an episode, obtains recent context,
extracts entity nodes and facts, resolves nodes against the existing
graph, embeds new material, and persists nodes/edges. Extracted
relationships carry the episode UUIDs that support them.
Temporal edge handling is the standout step. Relationship extraction
can produce valid_at and invalid_at;
maintenance code compares a resolved edge with existing candidates and
expires or bounds overlapping claims. An older fact can remain in the
graph with a closed validity interval instead of being deleted.
Bulk ingestion reduces calls through combined extraction but documents a tradeoff: it omits some edge invalidation/date extraction behavior found in single-episode insertion. Consumers cannot assume the faster path is semantically identical.
8. Agent Integration
The Python library is the primary surface. MCP and server packages
expose graph operations to agents and applications.
group_id is the main namespace and must be supplied
consistently; the MCP server supplies a configured default.
Search returns structured graph objects rather than a prebuilt prompt block. That is flexible for applications that want facts, entity summaries, or source episodes separately, but safe formatting, token budgeting, and injection fencing remain application responsibilities.
9. Reliability, Safety, and Trust
Strong safeguards:
- source episode UUIDs on extracted facts;
- retention of invalidated history;
- explicit event-time and ingestion-time fields;
group_idstamped on every node and edge, with FalkorDB placing each group in its own database;- constraints and indexes per graph backend;
- bounded concurrency helpers and retry-capable model clients;
- validation of LLM-returned entity names against resolved nodes;
- dropping self-edges;
- extensive driver, search, extraction, dedupe, and temporal tests.
Trust remains evidence-oriented rather than verification-oriented. A
fact can be well sourced and still false. LLM entity resolution can
merge distinct real-world entities; a bad merge then contaminates many
neighboring facts. Group IDs are a filter the caller chooses:
Graphiti.search(group_ids=None) and the underlying queries
apply no group predicate when none is passed, so the library searches
every group. That is why scope_enforced is not carried;
authorization, and the decision to scope, belong to the service around
the library.
Concurrent writes could cross groups until
September. add_episode and
add_episode_bulk reassigned the shared
self.driver whenever a group mapped to a different
database, and a concurrent call for another group could swap it
mid-ingestion, so episodes were persisted to the wrong FalkorDB graph
with no error (#1676). Since 0.30.2 a request-scoped driver
and client bundle is threaded through both paths
(_resolve_request_scope, graphiti.py:1013),
clear_data by group also removes Saga nodes, and Neo4j
queries are routed to the configured database.
10. Tests, Evals, and Benchmarks
The repository has unit and integration coverage for graph operations, entity/edge extraction, deduplication, temporal invalidation, search recipes, filters, drivers, bulk ingestion, episode deletion, and sagas. Many meaningful tests require a live graph database and model substitutes.
There are benchmark and evaluation utilities, but measured quality depends on the LLM, embedder, graph backend, ontology, and dataset. The source gives strong evidence for mechanics; it does not make temporal extraction or entity resolution objectively correct.
11. For Your Own Build
Steal
- Store episodes as evidence before graph-derived facts.
- Distinguish transaction time from real-world validity time.
- Invalidate an old fact by closing its interval rather than erasing history.
- Keep source episode IDs on every derived relationship.
- Search facts, entities, episodes, and communities as separate scopes.
- Make retrieval recipes explicit and task-selectable.
- Validate LLM-extracted relationships against resolved entity identities.
- Maintain both wall-clock and event-time watermarks for backfilled summaries.
Avoid
- LLM errors can mutate graph topology, not just one text record.
- A mistaken entity merge has wide blast radius.
- Graph database and model dependencies make local operation heavier than file/SQLite memory.
- Single and bulk ingestion do not provide identical temporal semantics.
- Search configuration has many degrees of freedom and can be hard to evaluate.
- No built-in candidate/verified/rejected trust state.
- Prompt assembly and authorization are outside the core library.
Fit
Borrow Graphiti when time and relationships are central to the domain: changing employment, ownership, preferences, plans, incidents, or other facts that have valid intervals. The bi-temporal edge model and evidence links are more valuable than copying the whole ingestion pipeline.
For simpler personal memory, a graph may be needless complexity. Preserve source events and add validity fields to ordinary records first. Adopt entity resolution and graph traversal only when cross-entity questions measurably require them.
12. Open Questions
- How often does automatic edge invalidation close the wrong historical fact?
- What recovery path splits entities after an incorrect deduplication?
- How should applications authorize
group_idrather than merely filter by it? - Which search recipe performs best for conversational recall versus graph investigation?
- How should bulk ingestion advertise its reduced temporal semantics to callers?
- Can source episodes be deleted for privacy without leaving unsupported derived edges?
Appendix: File Index
graphiti_core/graphiti.pygraphiti_core/nodes.pygraphiti_core/edges.pygraphiti_core/utils/maintenance/node_operations.pygraphiti_core/utils/maintenance/edge_operations.pygraphiti_core/utils/maintenance/combined_extraction.pygraphiti_core/utils/maintenance/graph_data_operations.pygraphiti_core/search/search.pygraphiti_core/search/search_config.pygraphiti_core/search/search_config_recipes.pygraphiti_core/search/search_filters.pygraphiti_core/driver/mcp_server/tests/
History
2026-09-15 — c035afb7…
— 45 commits on, 2026-09-11, graphiti-core 0.30.2. Screened before
reading: no auto-run surface, four build-time execution points, one
unpinned surface, three dependency surfaces inside the cooldown and two
agent-instruction files read as data; nothing was installed or run. The
temporal edge model is unchanged. The memory-relevant fixes: a
request-scoped driver stops concurrent add_episode calls
for different groups from writing into each other's FalkorDB graph
(#1676); group-scoped clear_data now removes Saga nodes;
Neo4j queries honour the configured database; prior node attributes
survive when no entity type applies; FactResult returns
source and target node ids and episodes; and the edge cross-encoder
shortlist merges balanced. scope_enforced is withdrawn:
search applies group_ids only when a caller passes them,
and none is required. bitemporal kept with an evidence
record. One mark.
2026-08-31 — 425bf248…
— citation audit at the same pin. The 2026-08-06 entry named only the
FalkorDB driver as the source diff, understating a 44-file range that
also reaches graphiti.py, edge_db_queries.py,
search_utils.py, community_operations.py,
three other drivers, the MCP server and three new test files; that entry
is corrected. The load-bearing part of it holds —
edge_operations.py and node_operations.py are
untouched, and "24 commits on" is exact. No body text or mark
changed.
2026-08-06 — 425bf248…
— 24 commits on, 44 files, and the memory mechanism is not among them.
The largest source piece is the FalkorDB driver — a new
fulltext.py, with operations/graph_ops.py,
operations/search_ops.py and
falkordb_driver.py cut back against a widened shared
graph_operations interface and matching
graph_ops.py trims in the kuzu, neo4j and neptune drivers —
but the diff is wider than that: it also touches
graphiti.py, models/edges/edge_db_queries.py,
search/search_utils.py,
utils/maintenance/community_operations.py and the MCP
server's config, factories and entry point, adds three test files
(test_remove_communities.py,
test_edge_bfs_query_shape.py,
test_edge_db_queries.py), and hardens CI with a Socket
firewall action. What it leaves alone is the part this report is about:
edge_operations.py and node_operations.py are
unchanged, so bi-temporal edge validity, entity resolution and the
invalidation path are as described and no published claim is stale. Both
marks re-checked in both directions and neither moves. Screened again: 0
auto-run surfaces, 4 build-time exec paths, nothing inside the
cooldown.
2026-07-26 — 9140123a…
— first reading.