
Stateful AI agents need a memory layer that survives context compaction, session restarts, and tool calls without re-deriving everything they have already learned. A knowledge graph fits that role by storing entities, relationships, and episodes in a structure the agent can both read and write. This article walks through the practical design of that loop using Graphiti's EntityNode model and episode pipeline as concrete scaffolding. The focus is on how an agent queries context before acting and how it folds new observations back into the graph afterwards, with notes on how short, medium, and long-term memory map onto different node and edge types.

A single LLM call has no memory of the calls that came before it. The model sees only the tokens assembled into its current prompt; once the response is produced, anything not in that prompt is effectively gone. For a stateful agent — one that runs across sessions, tool calls, and context compaction — this is a hard ceiling. The agent cannot accumulate knowledge, learn from its own mistakes, or refer back to a decision it made an hour ago. Every fresh prompt has to either re-derive state from scratch or be handed a pre-built slice of context, and that slice is the agent's memory layer.
A vector store gives the agent a flat ranked list of similar chunks. It answers what sounds like my question? — useful, but bounded. A knowledge graph answers a different question: what is connected to this? Every fact sits on a labelled edge between two nodes, and traversal walks from one node to its neighbours to the neighbours of those neighbours. In practice that returns 3–4 hops of connected context, the structure an agent needs to follow a chain like customer X uses product Y → product Y had incident Z → incident Z resembles case W.
Benchmarks published in the GraphRAG-Bench paper (arXiv 2506.05690) put numbers on the intuition: graph traversal reaches 53.4% accuracy on multi-hop reasoning tasks against 42.9% for vector-only retrieval (Graph Engineering for AI Agents, 2026). Graphs also carry explainability — the path from question to answer is a literal path you can read out — and an explicit concept of identity, where "Marta" and "Marta S." can be reconciled into one node rather than two false neighbours in an embedding space.
A graph accommodates the three flavours of long-term memory that cognitive-science taxonomies map onto agents, without needing three separate systems:
Because all three share the same edge schema, an agent can fold a new observation in once and let it propagate to whichever memory type it belongs to.
A sibling piece in this series compares Graph RAG against naive RAG at the retrieval layer. This article is narrower: the graph is treated as the agent's memory substrate, not as a retrieval add-on. Graphiti is used throughout as concrete scaffolding because it ships an explicit EntityNode schema (with name embedding, summary, dynamic labels, and group_id partitioning) and a documented episode pipeline that extracts entities and relationships from new text and folds them in incrementally (Building AI Agents with Knowledge Graph Memory). The patterns are not unique to Graphiti — any temporal knowledge-graph framework with an entity model and an ingestion loop surfaces the same design questions, which the rest of this article works through.

EntityNodeA node in Graphiti is the smallest durable unit an agent reads or writes. Its concrete shape comes from the EntityNode class, which inherits a small set of identifying fields from a base Node and adds three entity-specific ones:
uuid: a fresh uuid4() minted at creation, used as the graph's internal handle.name: the canonical string for the entity (e.g., "Alice", "Acme Corp"). This is the surface used for matching and display, and it is what gets embedded.group_id: the partition namespace. Nodes in different group_id values are logically isolated, which lets a single graph store separate tenants, users, or projects without bleed-through.labels: a list of dynamic type tags, assigned by the LLM at extraction time rather than from a fixed enum.name_embedding: a vector embedding of the name, enabling semantic lookup when exact string matching is too brittle.summary: a short string summarizing the node's surrounding edges — a regional blurb rather than a global description, which keeps it cheap to regenerate as neighbors change.attributes: a free-form dict[str, Any] whose keys and value types depend on the node's labels. There is no schema validation at write time.created_at: a UTC timestamp recorded when the node first appears.EntityEdgeRelationships are first-class objects stored as EntityEdge records rather than bare (a)-[r]->(b) triples. Each edge carries:
name: the relationship type, again chosen by the LLM ("WORKS_FOR", "BORN_IN").fact: a natural-language sentence describing the relationship.valid_at / invalid_at: optional temporal validity markers pinning the fact to a lifespan.episodes: references back to the source episodes that established or reinforced the edge.This is what gives the graph its auditability. When a fact is superseded, the old edge is closed by setting invalid_at; the record is preserved rather than deleted, so the agent can still answer "what was true last spring" without contradiction (Zep).
Graphiti intentionally ships with no closed type system. Entity classification is dynamic and LLM-driven per episode, and the resulting labels list grows organically. The same real-world entity mentioned across episodes is merged via name matching rather than a hand-curated ontology, and a single EntityNode can simultaneously carry Person and Employee as context accumulates (Graphiti guide). Attributes behave the same way: keys appear when an episode justifies them, and outdated values are not overwritten in place — they move to a new edge whose validity window supersedes the prior one.

add_episode Entry PointWhen an agent decides an observation is worth remembering, it serializes that observation into an episode_body string and calls client.add_episode(...). From the agent's perspective, this is a single write. Internally, Graphiti hands the payload to a six-stage extraction pipeline that incrementally updates the graph rather than recomputing it from scratch.
The pipeline executes sequentially on each new episode:
EntityNode and EntityEdge objects into the underlying graph store (typically Neo4j).Because each call only touches the slice of the graph relevant to the new episode, writes complete at a predictable latency and the graph can grow alongside the agent's activity without global recomputation.
EpisodeType VariantsGraphiti supports three episode sources through the EpisodeType enum:
EpisodeType.text — free-form prose such as a user introduction or a tool-output summary.EpisodeType.message — multi-turn chat transcripts, where speaker turns matter for attribution.EpisodeType.json — structured payloads (metrics, document interactions, audit records) serialized as JSON.Choosing the right variant helps the extractor segment the input correctly; message episodes preserve speaker attribution that plain text loses, and JSON episodes give the model an unambiguous schema to ground extraction in.
The recommended pattern is to write one episode per distinct event — user_login_success, user_profile_update, and so on — rather than batching a day's activity into a single daily_summary body. Narrow, well-labeled episodes give the extraction LLM a coherent context window and produce higher-quality entities and edges than broad summaries, where unrelated facts blur together and edge quality drops.
After extraction, Graphiti performs entity resolution to decide whether, for example, "Alice from engineering", "Alice Smith", and "the new tech lead Alice" all refer to the same person. The merge is driven by the extraction LLM combined with basic name-matching heuristics. The documented caveat is that the same model can extract slightly different entity sets for similar episodes, and different models will diverge further. Writes are therefore best-effort: a node may be missed, an edge may be redundant, and a near-duplicate may slip through. Treat the graph as an eventually-consistent memory that sharpens as more observations accumulate.

Before an agent acts, the memory layer has to answer a single question cheaply: what slice of the graph is relevant to this turn? In a Graphiti-backed setup, that question is answered by a hybrid query that fuses three signals — dense vector similarity, BM25 keyword matching, and graph traversal — into a single ranked result. Crucially, none of these signals require an LLM call at retrieval time. The extraction LLM runs on the write side when episodes are ingested; the read side stays non-generative so it can sit inline behind every chat turn.
Zep reports that this hybrid query returns results in under a second at the 95th percentile, which is fast enough to drop in between the user message and the model response without the agent feeling laggy. This is the practical difference from Microsoft's GraphRAG, whose community-summarisation pipeline can take tens of seconds on a fixed corpus — fine for offline analysis, unusable for a live agent loop.
The practical read sequence looks like this:
name_embedding field on EntityNode for semantic matches and the full-text index for exact or near-exact hits. group_id lets the query stay scoped to a tenant, user, or session partition.EntityEdge connections one to three hops out pulls in the immediate neighbourhood — collaborators, related tickets, recent episodes — without dragging the whole graph back.summary and relevant attributes.The read path is also where context engineering actually happens. A graph can hold arbitrarily many facts, but a prompt window cannot. The agent decides which of the returned nodes and edges deserve tokens, in what order, and how to serialise them so the model reasons over grounded facts rather than hallucinating across an unindexed prompt. Two design choices follow from that:
"Alice — works_on — Project Helios — has_episode — 'kickoff 2026-08-12'" keeps the relational evidence intact and lets the model cite it, rather than dumping a paragraph that loses which fact connects to which entity.Done well, the read path is cheap, deterministic, and grounded. The model never sees the full graph; it only sees the slice the retrieval layer has already judged relevant.

In Graphiti, the raw unit of what-happened is an episode: a discrete, timestamped input such as a chat message, a JSON event, or a document snippet, ingested via add_episode with a reference_time and a source_description (Building AI Agents with Knowledge Graph Memory). Each episode is processed incrementally: entities are extracted, EntityEdges are proposed, and the bi-temporal model records both when the event occurred and when the system learned about it. The edges carry a fact, a relationship name, and optional valid_at / invalid_at markers that let an old connection be closed rather than overwritten when it stops being true. Episodic memory, then, lives in the durable trail of episodes plus the temporal EntityEdges they generate.
Semantic memory maps onto the EntityNode itself. Each node has a stable name, dynamic labels chosen by the LLM during extraction (for example ["Person", "Employee"]), an attributes dict whose shape depends on the labels, and a summary field described as a regional summary of surrounding edges (Building AI Agents with Knowledge Graph Memory). When the same real-world entity appears across many episodes, name matching collapses the mentions into one node, and the accumulated facts thicken its summary. The result is a consolidated "what is true about X" record rather than a replay of the conversations that taught the agent about X.
Procedural memory is what works. The cognitive-science taxonomy treats this as a distinct tier alongside episodic and semantic memory (Knowledge Graphs for AI Agents). In a Graphiti-backed agent it lives not in a dedicated type but in subgraph patterns: a tool-call sequence whose EntityEdges form a successful path, or a cluster of nodes that the agent traverses repeatedly when solving a known class of task. These patterns are retrieved as a slice rather than re-derived, and they can be replayed as a workflow template.
summary and attributes have stabilized across many episodes, surviving context compaction, session restarts, and tool calls.Promotion between tiers should not be an automatic side effect. Folding a fresh episode into a stable summary, closing an EntityEdge's valid_at, or elevating a recurring tool-call path into a reusable procedural template is an editorial choice with costs: it commits the agent to a view of the world. In practice that decision belongs either to the agent itself when confidence is high, or to a background consolidation job that periodically reviews recent episodes and rewrites the affected summaries. Treating promotion as deliberate keeps memory honest and lets the team audit what the agent has decided to remember.

On the read path, the agent typically translates its goal into a graph query — usually openCypher or a constrained traversal exposed through the Model Context Protocol — and submits it to a Cypher-enabled MCP server such as the one Neo4j provides with tools like get_neo4j_schema, read_neo4j_cypher, and write_neo4j_cypher (Neo4j developer blog). The grounding step sits between generation and execution: every query must be validated against the live schema the MCP server returns before it touches the database.
If the query references a node label or relationship type that does not exist, the server rejects it and returns structured feedback — typically a parser error or a schema-mismatch message naming the offending token. The agent uses that payload to repair the query (swap the label for one the schema actually defines, rewrite the traversal pattern, or fall back to a vector search if no structural match exists) and resubmits. This loop is what stops an LLM from hallucinating entity types the graph never saw, and it is the same validation pattern MCP tools are expected to enforce: reject malformed inputs, sanitize parameters, and never let unvalidated strings reach the database (TrueFoundry, MCP spec). A common concrete failure: an agent writes MATCH (p:Customer) against a graph where customers are labeled Person with an is_customer attribute, the server rejects the label, and the retry replaces it with the correct pattern.
The write path needs the same skepticism. In Graphiti's episode pipeline, the EpisodeType.text, EpisodeType.message, and EpisodeType.json inputs are preprocessed and passed through entity and relationship extraction, then a consistency-checking step reconciles the candidates against the existing graph (Medium guide). Because Graphiti does not hard-code entity types — labels are assigned dynamically by the LLM during extraction — the surface for schema drift is real. Different models, or the same model on a noisy episode, can produce labels the rest of the system has never seen, or assign attributes that contradict an existing EntityEdge.
The reliable pattern is to refuse to mutate shared state directly on suspicion. Episodes whose extracted entities fall outside the declared label set, or whose attributes conflict with an existing node's summary or EntityEdge facts, should be routed to a verification step — a second-pass extraction, a confidence threshold check, or a human-in-the-loop review — before they are committed. When a contradiction is real rather than noisy, Graphiti's bi-temporal model handles it by stamping the old fact with a supersession timestamp instead of deleting it, preserving history while still surfacing the new ground truth (The Neural Maze). The discipline is symmetric on both ends of the loop: reads fail closed when the schema disagrees, and writes fail open only after they have been checked against the schema they are about to change.

In Graphiti, every EntityNode carries a group_id field that pins it to a single namespace, and every add_episode and search call can be scoped to one or more group_ids (source). In production this is the lever that keeps tenants, users, or projects from contaminating each other: route all of Alice's episodes through group_id="user_alice", all of Acme's through group_id="tenant_acme", and let the search call filter accordingly so one user's preferences never surface in another user's context window.
The partitioning model is intentionally flat. Only a single-level group_id is supported, with no built-in hierarchy such as organization to team to user, and no automatic sharding across groups (source). That has two practical consequences:
group_id string or by mirroring it as relationship edges rather than as partitions.group_id; if they must each keep a private scratchpad alongside shared facts, that means two group_id values per agent with explicit cross-partition reads.A graph that is accurate on day one drifts silently unless the loop is actively maintained. Three failure modes recur in production (source):
Graphiti's temporal edges help with the first two, since facts carry validity windows rather than a single frozen state (source), but they do not eliminate the underlying problem of propagation.
Two complementary disciplines catch drift before it leaks into agent output (source):
valid_until has passed without a successor episode, and label distributions for new entity types that did not exist a week ago.Pair both with the production hygiene already standard for the pipeline layer: batching episode additions, batching reads to avoid overwhelming Neo4j, and keeping group_id discipline consistent across callers (source).
Every agent action starts the same way: read a connected slice of the graph scoped to the right group_id, act on it, and write the resulting observation back as a new episode so the extraction pipeline can fold entities, edges, and temporal facts into the store. The loop is what makes the graph the agent's accumulating memory, and discipline around partitioning and freshness is what keeps the loop honest turn after turn (source).