Graphiti add_episode: Parameters, Indices and the Errors I Hit

September 18, 2026 · 8 min read · knowledge-graph, graphiti, falkordb, python

add_episode is Graphiti’s write call. It takes one episode, a piece of text, a JSON document or a few chat lines, has the language model extract entities and facts, resolves them against what the graph already holds, and stores each fact with the time it became true. Call build_indices_and_constraints() once before the first one.

Last tested: September 2026, against the graphiti-core 0.30.2 source and the graphs I run on FalkorDB and on an embedded LadybugDB.

add_episode cheat sheet

QuestionAnswer
Importfrom graphiti_core.nodes import EpisodeType
Before the first writeawait graphiti.build_indices_and_constraints(), once, inside your async main
Required argumentsname, episode_body, source_description, reference_time
Episode typesEpisodeType.text, EpisodeType.json, EpisodeType.message (the default)
group_idASCII letters, digits, - and _. On FalkorDB it names a separate graph
Searching a group on FalkorDBA driver created for that group, or one cloned to it
ConcurrencyOne call per episode under a semaphore, graphiti-core 0.30.2 or later when groups differ
add_episode_bulkOnly with a model that reliably fills structured output
Proof it landedCount result.nodes and result.edges for every episode

The setup that has held in my graphs: create the driver for the group you write, build the indices once, then call add_episode for each episode.

import asyncio
from datetime import datetime, timezone

from graphiti_core import Graphiti
from graphiti_core.driver.falkordb_driver import FalkorDriver
from graphiti_core.nodes import EpisodeType


async def main():
    driver = FalkorDriver(host="localhost", port=6379, database="acme")
    # Graphiti uses OpenAI by default; pass llm_client and embedder for anything else.
    graphiti = Graphiti(graph_driver=driver)
    await graphiti.build_indices_and_constraints()

    result = await graphiti.add_episode(
        name="ticket-4711",
        episode_body="Acme moved its go-live from 1 October to 15 October.",
        source=EpisodeType.text,
        source_description="support ticket",
        reference_time=datetime(2026, 9, 17, 9, 30, tzinfo=timezone.utc),
        group_id="acme",
    )
    print(len(result.nodes), "entities,", len(result.edges), "facts")

    await graphiti.close()


asyncio.run(main())

When the driver’s database and the group_id carry the same name, writes and searches stay in one graph. The next sections cover what happens when they do not.

build_indices_and_constraints: once, before the first add_episode

build_indices_and_constraints() creates the range indices and the four fulltext indices that search runs on: node_name_and_summary, edge_name_and_fact, episode_content and community_name.

Call it yourself. The FalkorDB driver schedules the build on its own only when it is created inside a running event loop, so a driver created at module level, before asyncio.run, builds nothing. And when add_episode first writes to a new group, the driver it creates for that group’s graph starts its own index build in the background, at the same time as your first writes. One awaited call per graph at startup covers both cases.

Repeating it is harmless: on FalkorDB an existing index is logged and skipped, and my ingest scripts call it at the start of every run. delete_existing=True drops the indices and builds them again, which takes a while on a large graph.

add_episode parameters that matter

ParameterWhat it does
nameA label for the episode, such as a ticket id or a file name
episode_bodyThe content: plain text, a JSON string, or chat lines
source_descriptionWhere the content came from, such as “support ticket”. Stored on the episode and passed to the model during extraction
reference_timeWhen the content was said or true. Stored as the episode’s valid time and used as the reference point when facts are dated. Pass a timezone-aware UTC datetime
sourceEpisodeType.text, .json or .message. The default is message
group_idThe partition. Anything besides ASCII letters, digits, - and _ raises GroupIdValidationError. On FalkorDB, a separate graph
previous_episode_uuidsContext for extraction. Left at None, Graphiti loads up to 10 earlier episodes of the same group and source type before reference_time. A list of uuids uses those instead
update_communitiesAlso refreshes community summaries, at the cost of more model calls per episode
entity_types, edge_types, edge_type_map, excluded_entity_typesYour own ontology, as Pydantic models
custom_extraction_instructionsExtra instructions for the extraction prompt
uuidProcesses an episode that already exists again, instead of creating a new one
saga, saga_previous_episode_uuidChain episodes into an ordered saga

add_episode returns an AddEpisodeResults with episode, nodes (entities), edges (facts), episodic_edges, communities and community_edges.

EpisodeType: text, json or message

Each type has its own extraction prompt, so the choice changes what the model extracts.

import json
from datetime import datetime, timezone

from graphiti_core.nodes import EpisodeType

# Chat or meeting lines: one "speaker: text" per line.
await graphiti.add_episode(
    name="standup-2026-09-17",
    episode_body="Anna: The Acme import is blocked on SSO.\nBen: I will ask their IT today.",
    source=EpisodeType.message,
    source_description="team chat",
    reference_time=datetime.now(timezone.utc),
    group_id="acme",
)

# A record from another system: a JSON string, not a dict.
await graphiti.add_episode(
    name="crm-acme",
    episode_body=json.dumps({"account": "Acme", "stage": "pilot", "owner": "Anna"}),
    source=EpisodeType.json,
    source_description="CRM account record",
    reference_time=datetime.now(timezone.utc),
    group_id="acme",
)

message is the default, so a document sent without source= goes through the chat prompt, which looks for speakers. Set EpisodeType.text for documents, tickets and wiki pages.

group_id on FalkorDB: writes go to the group’s graph, search stays put

On Neo4j, group_id is a property inside one database, and search(..., group_ids=["acme"]) filters on it. On FalkorDB it is a separate graph: add_episode points a copy of the driver at a graph named after the group. Without a group_id, FalkorDB writes land in the group _ of default_db.

Graphiti.search reads through the instance’s own driver. Write group acme from an instance whose driver points at default_db, and a search for acme on that instance comes back empty, with no error. Two ways out: create the driver with database="acme", as in the setup above, or search through a driver cloned to the group.

acme = graphiti.driver.clone(database="acme")
edges = await graphiti.search("When does Acme go live?", group_ids=["acme"], driver=acme)
for edge in edges:
    print(edge.fact, edge.valid_at, edge.invalid_at)

My own kg command line takes the second way: one shared Graphiti instance, and a driver cloned to the domain for every read.

Concurrent add_episode: what held and what broke

My first ingest script awaited one episode at a time: about 2 minutes per item, close to 7 hours for 200. The same items with one add_episode call each, under a semaphore, took about 10 seconds per item and around 35 minutes in total.

sem = asyncio.Semaphore(8)


async def ingest(item):
    async with sem:
        return await graphiti.add_episode(**item)


results = await asyncio.gather(*(ingest(item) for item in items))

I ran 14 at once, matching the 14 model workers behind it. Size yours to your model’s rate limit. Three limits apply:

  • Different groups at once need graphiti-core 0.30.2 or later. Up to 0.30.1, add_episode switched the shared driver to the group’s graph, so a concurrent call for another group could land its writes in the wrong graph, silently (issue 1676). 0.30.2 gives every call its own driver. On older versions, keep one group per process.
  • An embedded LadybugDB takes one writer. On LadybugDB, which I run through Graphiti’s Kuzu driver, a second writing process fails with IO exception: Could not set lock on file, and a second writer in the same process with Cannot start a new write transaction in the system. Only one write transaction at a time. Let the producers queue, and let one applier write.
  • SEMAPHORE_LIMIT is a separate knob. It caps the parallel model and database calls inside each add_episode: 20 by default in graphiti-core, 10 in the official MCP server. Lower it when your provider starts returning 429 errors.

add_episode_bulk: the dedup step that broke

add_episode_bulk takes a list of RawEpisode objects and resolves duplicates across the whole batch in fewer, larger model calls. On my graphs, with a small model, that dedup prompt came back with the JSON schema instead of the data, and the run crashed on the first dedup call with a pydantic ValidationError: NodeResolutions.entity_resolutions was missing.

Per-episode add_episode under a semaphore resolves each episode against the graph on its own, and has run clean since. Try the bulk call only with a model that reliably fills structured output, and on a sample first.

Errors I hit, and the fix

SymptomCauseFix
GroupIdValidationErrorA group_id with spaces, dots or umlautsASCII letters, digits, - and _ only
Search returns nothing right after add_episode, on FalkorDBThe search read default_db, the episode went to the group’s graphSearch through a driver for that group
Search returns nothing after an upgradeThe reader runs a different graphiti-core version than the writer; one patch apart returned nothing in my graphsPin every reader to the writer’s version
Vectors match nothing, with no errorThe index width differs from the embedding model’s, 768 for nomic-embed-textSet EMBEDDING_DIM and embedding_dim to the model’s width (details)
Dedup queries killed once the graph grewMy FalkorDB ran with a 1,000 ms query timeoutRaise it with TIMEOUT in FALKORDB_ARGS, as below
The graph is empty after the container was recreatedThe data sat in the container layer, not in the mounted volumeMount the volume at /var/lib/falkordb/data
ValidationError on NodeResolutionsadd_episode_bulk with a small modelPer-episode add_episode
Could not set lock on file, Only one write transaction at a timeA second writer on LadybugDBOne writer, and a queue in front of it

The FalkorDB settings from the two rows above, as they run on my server:

services:
  falkordb:
    image: falkordb/falkordb:latest
    environment:
      FALKORDB_ARGS: "TIMEOUT 600000"
    volumes:
      - falkordb_data:/var/lib/falkordb/data

volumes:
  falkordb_data:

Prove every episode landed

add_episode returns what it wrote. Log len(result.nodes) and len(result.edges) for every episode, and keep a cursor of what went in, so a crashed run resumes where it stopped instead of writing the same episodes twice. My ingest writes its cursor every 10 episodes.

The production decisions around this call, backends, models, embeddings and versions, are in Graphiti in production, and the ideas underneath in the temporal knowledge graph guide. Graphiti Local wraps the read side in six read-only MCP tools and a kg command line, with a write path that waits for a person. For a context layer built this way on your own tenant, see knowledge graph consulting.

Graphiti Local is an independent community project built on Graphiti. It is not affiliated with or endorsed by Zep. Versions and defaults as of 18 September 2026.

Changelog

  • 18 September 2026: First version, checked against the graphiti-core 0.30.2 source.

Frequently asked questions

What does add_episode do in Graphiti?

It is Graphiti's write call. It takes one piece of text, JSON or a few chat lines, has the language model extract entities and facts, resolves them against what the graph already holds, and stores each fact with the time it became true. It returns the episode, the entities and the facts it wrote.

Do I need to call build_indices_and_constraints before add_episode?

Call it once at startup, inside your async main, before the first add_episode. It creates the range and fulltext indices that search runs on. The FalkorDB driver schedules the build by itself only when it is created inside a running event loop, so an explicit call is the safe default, and repeating it is harmless.

How do I import EpisodeType?

from graphiti_core.nodes import EpisodeType. It has three values: EpisodeType.text for documents and tickets, EpisodeType.json for a JSON string, and EpisodeType.message for chat lines written as "speaker: text". The default is message.

Why does search return nothing after add_episode on FalkorDB?

On FalkorDB each group_id is a separate graph. add_episode writes into the graph named after the group, but Graphiti.search reads through the instance's own driver, which points at default_db unless you set another database. Search through a driver cloned to the group, or create the driver with that database.

Can I run add_episode concurrently?

Yes, one call per episode under an asyncio semaphore. In my graphs that took ingestion from about 2 minutes per item to about 10 seconds. Up to graphiti-core 0.30.1, concurrent calls for different group_ids on FalkorDB could write into each other's graph, and an embedded LadybugDB accepts one writer at a time.

Should I use add_episode_bulk?

Only with a model that reliably fills structured output. With a small model, its batch dedup step returned the JSON schema instead of data and crashed with a pydantic ValidationError on the first call. Per-episode add_episode under a semaphore has been the reliable path in my graphs.

Book a 30-minute architecture call
The context layer for your AI agents

Your agents answer from whatever the retriever finds, and too often that is last quarter's truth. I build the context layer they answer and act from: a temporal knowledge graph that keeps every fact with its source and the time it held, reads with each person's own permissions, and writes nothing without a person's approval. On your own tenant, billed by the hour, step by step.

Get your AI pilot checked