LangGraph integration
Add durable, cross-session memory to LangGraph agents
LangGraph agents using Zep gain durable, cross-session memory backed by a temporal knowledge graph. The zep-langgraph package wires Zep into your graph nodes: it provisions the user and thread, injects the user’s context block into the system prompt, persists each turn, and exposes a graph-search tool the model can call on demand.
A complete notebook example is available in the Zep repository.
Core benefits
- Context injection: Fold the user’s context block into the system prompt via a
promptcallable, or guarantee it with apre_model_hook - Per-turn persistence: Write each conversation turn back to Zep with a single helper
- On-demand graph search: Expose a LangChain tool the model calls to search the knowledge graph, with pin-or-expose control over its parameters
- Idempotent provisioning: Create the Zep user and thread out-of-band with
ensure_userandensure_thread - Custom context building: Replace the default context retrieval with your own
context_builder BaseStoresupport: UseZepStoreforcreate_react_agent(store=...)and langmem’s memory tools- Async and sync clients: Every helper has both an async and a synchronous variant
- Graceful degradation: A Zep failure is logged but never crashes the host agent
How it works
The package ships two layers. The node and tool helpers (the primary path) call Zep directly inside your graph nodes; this matches Zep’s recommended LangGraph pattern. ZepStore (secondary) is a BaseStore implementation for callers who need one — e.g. create_react_agent(store=...) or langmem’s memory tools.
The Zep loop is the same everywhere — create user, create thread, add messages, retrieve context — and each step is wrapped as a helper you call from a graph node:
ensure_user/ensure_thread— idempotently provision the Zep user and thread before the first turn. See provisioning users and threads.build_system_message— fetches the context block (thread.get_user_context) assembled from the entire user graph and folds it into aSystemMessagewith your base instructions, ready to prepend to the model’s message list.get_zep_contextreturns just the raw block. Both accept acontext_builderthat replaces the default retrieval.create_zep_pre_model_hook— builds apre_model_hookforcreate_react_agentthat injects context on every model call without relying on apromptcallable. See guaranteed context injection.persist_messages— wrapsthread.add_messages. Accepts LangChainBaseMessageobjects (converted automatically) or native ZepMessageobjects, flattens multimodal content to text, and maps names so Zep can resolve identity. Zep rejects direct thread-message payloads over 4,096 characters or 30 messages per call; this helper truncates over-long content and splits larger turns across multiple calls. Passreturn_context=Trueto fold persist and retrieve into one round-trip.create_graph_search_tool— returns a LangChainStructuredToolovergraph.search. Pass it tocreate_react_agent(tools=[...])and the model decides when to search. Exactly one ofuser_id(the user’s personal graph) orgraph_id(a shared standalone graph) is required and fixed at construction; the remaining search parameters are pin-or-expose. See controlling the search tool.
Identity is yours to manage, and the package never provisions lazily — create the Zep user and thread out-of-band before the first turn, with ensure_user / ensure_thread or your own SDK calls.
Installation
Requires Python 3.11+, langgraph>=1.2.5, zep-cloud>=3.23.0, and a Zep Cloud API key. Get your API key from app.getzep.com.
Set up your environment variables:
Upgrading from zep-langgraph 0.1.x
Two breaking changes affect existing code:
- Default context template: the context block is wrapped in
<ZEP_CONTEXT>...</ZEP_CONTEXT>instead of<MEMORY>...</MEMORY>. To keep the old wording, passtemplate="<MEMORY>\n{context}\n</MEMORY>"tobuild_system_messageorget_zep_context. - Search tool schema: the model can set
scope,reranker,limit,mmr_lambda, andcenter_node_uuid, which 0.1.x fixed at construction. Existingscope=/limit=constructor arguments keep their runtime behavior by pinning those parameters; usepinned_paramsto fix any parameter the model shouldn’t control.
See the package changelog for the full list of changes.
Usage
Provision the user and thread, inject context with a prompt callable, expose the graph-search tool, and persist each turn. See the runnable examples for additional patterns.
Guaranteed context injection with a pre-model hook
The prompt callable above shapes the model’s input, but nothing enforces that a caller wires one. create_zep_pre_model_hook builds a pre_model_hook for create_react_agent that injects context on every model call:
The hook (a ZepPreModelHook) fetches the context block — or runs a custom context_builder — and returns it via the hook’s llm_input_messages key. Per create_react_agent’s pre_model_hook contract, this shapes the model’s input for that step without overwriting the persisted messages state, so injected context is re-fetched fresh every turn rather than baked into thread history. The hook supports context_builder, template, template_id, and base_instructions, using the same retrieval path as build_system_message.
Choose the prompt callable when your node already assembles the message list and you want full control over it; choose the hook when you want injection guaranteed regardless of how the agent is wired, or to keep injected context out of persisted state. The hook only injects context — call persist_messages separately after the model responds to save the turn.
Provisioning users and threads
The package never creates users or threads lazily — provision both out-of-band before the first turn. ensure_user and ensure_thread are idempotent create-then-catch-conflict helpers: they call the Zep SDK’s create method, treat an “already exists” conflict as success (returning False), and let genuine failures (auth, network, 5xx) raise loudly rather than degrade silently — useful for onboarding flows that should stop on real errors.
The on_created hook (a UserSetupHook) fires only when the user is genuinely new — use it for one-time per-user setup. If the hook raises, the exception propagates even though the user was created, so make the hook idempotent. The synchronous twins ensure_user_sync / ensure_thread_sync take a synchronous Zep client and a synchronous hook (UserSetupHookSync). These are plain module-level functions with no instance caching — cache the “already provisioned” result yourself to skip redundant calls.
Custom context building
Pass context_builder to get_zep_context or build_system_message (or their _sync twins) to replace the default thread.get_user_context retrieval with custom logic — a filtered graph search, a different graph, or multiple combined sources:
ContextInput is a frozen dataclass bundling zep, user_id, thread_id, and user_message; the user_id and user_message keyword arguments populate it. A builder that raises is logged and treated as returning None — these helpers never raise.
Because the helpers are plain functions rather than a single framework-owned turn hook, they don’t run persistence and context building concurrently for you. To overlap the two, gather them yourself:
Customizing the context template
The context block is wrapped using DEFAULT_CONTEXT_TEMPLATE — an explicit <ZEP_CONTEXT>...</ZEP_CONTEXT> block, canonical across Zep’s framework integrations. Pass template= to customize the wording; it must contain a literal {context} placeholder. Pass template_id= instead to render a server-side context template.
format_context_block renders via plain string replacement (template.replace("{context}", context), never str.format), so context text or a custom template containing {, }, or % is always safe to inject.
Controlling the search tool
The search target is fixed when the tool is constructed — exactly one of user_id or graph_id. Every other graph.search parameter is pin-or-expose: exposed to the model in the tool’s schema by default (with documented defaults), pinnable to a constant, or hideable so Zep’s server-side default applies.
Model-exposed parameters:
pinned_paramsfixes a parameter to a constant value: hidden from the model’s schema, always sent.hidden_paramshides a parameter without pinning it, so Zep’s server-side default applies.- A parameter neither pinned nor supplied by the model is omitted from the
graph.searchcall entirely — never forwarded as an explicitNone. - The legacy
scope,reranker, andlimitconstructor keywords pin the corresponding parameter, equivalent topinned_params. search_filtersandbfs_origin_node_uuidsare constructor-only; their complex shapes are not exposed to the model.
The schema is built dynamically with pydantic.create_model and passed as the StructuredTool’s args_schema. See searching the graph for what each parameter does.
Long-term memory with ZepStore
BaseStore is LangGraph’s cross-thread long-term-memory interface; create_react_agent(store=...) and langmem’s memory tools require one. Zep is a temporal knowledge graph, not a key-value store, so it can’t faithfully serve exact-key reads or read-after-write on its own. ZepStore bridges this with a hybrid-delegate design: a backing key-value store (default InMemoryStore) serves exact-key get / put / delete synchronously, while every put is also ingested into Zep and search is routed to Zep’s semantic graph.search.
Zep ingestion is asynchronous. A value written with put is available immediately for exact-key get (served by the backing store), but its extracted facts are not instantly returned by search. ZepStore is the long-term memory layer, not the checkpointer, so graph execution and short-term state are unaffected.
Public API
MAX_MESSAGE_CHARS and MAX_MESSAGES_PER_CALL are useful when writing custom batching around persist_messages or persist_messages_sync.
Both an AsyncZep (async helpers, recommended) and a synchronous Zep client are supported. Reuse a single client instance.
Best practices
- Provision the user and thread out-of-band before the first turn with
ensure_user/ensure_thread— the package never creates them lazily - Pass real names to
persist_messagesso Zep can resolve the user’s identity node - Pin search parameters the model shouldn’t control with
pinned_params— e.g. a fixedscopeorlimit - Use the async helpers with
AsyncZepfor non-blocking nodes; the_syncvariants exist for synchronous graphs - Allow time for indexing — Zep extracts knowledge asynchronously, so facts from a turn are not instantly searchable
Next steps
- Explore customizing graph structure for advanced knowledge organization
- Learn about searching the graph and how to tune search
- See code examples for the
create_react_agentandZepStorepatterns