# How to add memory to LangGraph Agents

**Himanshu Sangshetti**  •  May 20, 2026

LangGraph is built around state. Each node in the graph reads the state, mutates it, and passes it on. Checkpointers persist that state. Threads scope it. The architecture is honest about what it is: a runtime for stateful agent workflows.

The gap is that the state and the threads are bound to the graph, not to the user. When the same user returns three weeks later in a new thread, their preferences and history start over. That is what an external memory layer is for, and Mem0's LangGraph integration is the most direct path through it.

#### **What LangGraph is**

LangGraph is a low-level orchestration framework and runtime for building long-running, stateful agents. It is built by the LangChain team and does not require LangChain itself. The two are usually paired (LangGraph for the flow, LangChain for the LLM and tool primitives) but the LangGraph runtime stands on its own.

The core abstractions are small. A `StateGraph` defines the agent's workflow. Nodes are Python functions that read state and return updates. Edges connect nodes. State is a `TypedDict` (or `MessagesState` for chat-shaped flows) that flows through the graph as nodes mutate it. Two special nodes, `START` and `END`, define the execution boundaries.

The framework is aimed at complex agent orchestration where the flow itself matters: branching, conditional handoffs, tool loops, human-in-the-loop checkpoints, and multi-agent coordination. For simpler one-shot agents LangChain ships prebuilt architectures. LangGraph is where the work goes when the agent's decision graph has multiple paths.

#### **How LangGraph handles memory natively**

LangGraph splits persistence into two layers. The split matters for what each one is and is not built for.

**Checkpointers** persist graph state. After every super-step, the runtime serializes a snapshot (channel values, next nodes to execute, metadata, timestamps, task info) and writes it through a `BaseCheckpointSaver` implementation. The shipped backends cover the usual range:

- `InMemorySaver` keeps checkpoints in process. Resets on restart.
- `SqliteSaver` (and `AsyncSqliteSaver`) writes to a local SQLite file. Good for development.
- `PostgresSaver` (and `AsyncPostgresSaver`) targets production. Survives restarts and scales horizontally.
- `langchain-azure-cosmosdb` adds Cosmos DB as a managed option.

**Threads** scope checkpoints. A thread is a unique ID that ties a sequence of checkpoints together. Pass `thread_id` in the `configurable` portion of the config on every call and the runtime resumes from the latest checkpoint for that thread. New thread, new state.

**Store** is the second persistence hook, separate from checkpointers. The `Store` interface persists data across threads as namespaced tuples and supports semantic search via an embedding model. Where checkpointers hold the in-flight state of one workflow, the store holds long-lived information the application wants any future thread to see.

Both abstractions are well-designed primitives. They are also primitives, not products. The application owns what gets put into them.

#### **Where LangGraph's built-in memory stops**

The native persistence layer is well-shaped for one specific job: durable graph state for stateful, long-running agents. Outside that shape, the gap shows.

- **Checkpointers persist graph state, not extracted facts.** A 50-turn conversation gets stored as 50 messages plus the surrounding channel data. Reconstructing what the user prefers from the raw history is the application's problem.
- **The Store is unopinionated.** Semantic search and namespacing are built in, but write logic, fact extraction, deduplication, reconciliation, and consolidation are not. Every team that moves from checkpointer to Store ends up writing the same fact-extraction layer.
- **InMemorySaver loses everything on restart.** `SqliteSaver` pins state to a single machine. `PostgresSaver` scales horizontally but still leaves the application owning the schema for facts versus raw history.
- **No multi-tenant isolation beyond `thread_id`.** Threads scope a single conversation. There is no built-in `user_id` or `agent_id` primitive. Multi-tenant apps stitch user identity into thread names or metadata and then have to enforce isolation themselves.
- **No fact extraction model.** What enters the store is what the application puts there. The "what is worth remembering" decision sits in agent code, which means another LLM call the application has to write, prompt, and pay for.

Mem0 is the integration LangGraph users reach for when the gap above is real. The wiring is shorter than the list of limits.

#### **How to add Mem0 to LangGraph**

The full setup is documented at [docs.mem0.ai/integrations/langgraph](https://docs.mem0.ai/integrations/langgraph). Install pulls LangGraph alongside the Mem0 client:

```bash
pip install langgraph langchain-openai mem0ai python-dotenv
```

The integration uses the standard `MemoryClient` and plugs into a normal `StateGraph`. The state carries the messages and the user identifier the memory layer scopes on:

```python
from typing import Annotated, TypedDict, List

from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from mem0 import MemoryClient

llm = ChatOpenAI(model="gpt-5-mini")
mem0 = MemoryClient()

class State(TypedDict):
    messages: Annotated[List[HumanMessage | AIMessage], add_messages]
    mem0_user_id: str

def chatbot(state: State):
    messages = state["messages"]
    user_id = state["mem0_user_id"]
    memories = mem0.search(messages[-1].content, filters={"user_id": user_id})
    context = "Relevant information from previous conversations:\n"

for memory in memories["results"]:
        context += f"- {memory['memory']}\n"

system_message = SystemMessage(content=(
        "You are a helpful assistant. Use the context to personalize your responses.\n"
        + context
    ))

response = llm.invoke([system_message] + messages)

mem0.add(
        [{"role": "user", "content": messages[-1].content},
        {"role": "assistant", "content": response.content}],
        user_id=user_id,
    )

return {"messages": [response]}
```

Graph wiring is the usual LangGraph pattern:

```python
graph = StateGraph(State)
graph.add_node("chatbot", chatbot)
graph.add_edge(START, "chatbot")
compiled_graph = graph.compile()
```

Three things change once the chatbot node owns its own memory:

- **Persistence outlives any thread.** A new `thread_id` next week still has access to every fact stored under `mem0_user_id`. The agent's continuity is keyed to the user, not to the run.
- **Fact extraction runs server-side.** The `add` call ships the exchange to Mem0 and an extraction model decides what to keep. The next call retrieves a short list of facts, not the raw transcript.
- **Per-user isolation is built in.** `user_id` scopes every search and write. `agent_id` and `run_id` provide narrower scopes when the application needs them.

The same pattern composes cleanly with multi-node graphs. A retrieval node can read memories before a planner node decides which tool to call. A reflection node can write a derived fact after an action runs. The state graph stays the place where flow lives. The memory layer stays the place where durable facts live.

#### **Final Note**

LangGraph gives every node a place to read and write state. The native persistence layer keeps that state alive across super-steps and threads. What it does not do is decide what is worth keeping past the run or scope facts to the user rather than the workflow. One Mem0 integration, a couple of node-level calls, and the graph picks up persistence keyed to who the user is rather than which run they came in on.

_Mem0 is an intelligent, open-source memory layer designed for LLMs and AI agents to provide long-term, personalized, and context-aware interactions across sessions._
