When building an AI agent, you may want it to create a file in one conversation turn and use that same file later.
For example:
- Turn 1: The agent writes study notes.
- Turn 2: The agent reads those notes and answers questions about them.
- Turn 3: A different user/session should not be able to access those notes.
This is exactly what Deep Agents’ StateBackend is designed for.
In this tutorial, we will walk through a small Python example that demonstrates how StateBackend, LangGraph checkpoints, and thread_id work together.
What You Will Learn
By the end of this article, you will understand:
- What
StateBackendis. - Why a LangGraph checkpointer is needed.
- How
thread_idseparates one agent session from another. - Why the same thread can access a file in later turns.
- Why another thread cannot see that file.
- Why
InMemorySaveris useful for testing but not production.
The Main Idea
Think of a thread_id as a unique conversation workspace.
Each workspace can have:
- Conversation history
- Agent state
- Files created through
StateBackend
Here is the concept visually:
┌──────────────────────────────┐
│ InMemorySaver │
│ (stores LangGraph state) │
└──────────────┬───────────────┘
│
┌─────────────────────┴─────────────────────┐
│ │
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ thread_id: │ │ thread_id: │
│ talha-learning-thread │ │ another-user-thread │
│ │ │ │
│ StateBackend files: │ │ StateBackend files: │
│ /learning/python_notes.md │ │ (empty) │
└───────────────────────────┘ └───────────────────────────┘
The important rule is:
Files written with
StateBackendbelong to the current LangGraph thread.
So, when the same thread_id is used again, the agent can access its previous files. When a different thread_id is used, it receives a separate state and cannot access files from the first thread.
Deep Agents documents StateBackend as a thread-scoped backend: files are stored in agent state, persist across turns through checkpoints, and are not shared between threads. (docs.langchain.com)
The Complete Example
Here is the code:
"""
Demonstrates Deep Agents' StateBackend.
What you will observe:
1. Turn 1 writes a file.
2. Turn 2 reads that same file using the SAME thread_id.
3. Turn 3 uses a DIFFERENT thread_id and cannot see the file.
"""
from deepagents import create_deep_agent
from deepagents.backends import StateBackend
from langgraph.checkpoint.memory import InMemorySaver
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
load_dotenv()
# ---------------------------------------------------------
# 1. Create an in-memory checkpointer
# ---------------------------------------------------------
# The checkpointer saves LangGraph state between agent calls.
#
# InMemorySaver is useful for local experiments.
# Its data disappears when this Python program stops.
checkpointer = InMemorySaver()
# ---------------------------------------------------------
# 2. Create the model with a max_tokens cap
# ---------------------------------------------------------
# This prevents the model response from using too many tokens.
model = init_chat_model(
"openrouter:nvidia/nemotron-3-super-120b-a12b",
max_tokens=4096,
)
# ---------------------------------------------------------
# 3. Create the agent with StateBackend
# ---------------------------------------------------------
# StateBackend is the default Deep Agents backend,
# but we specify it explicitly for learning purposes.
agent = create_deep_agent(
model=model,
backend=StateBackend(),
checkpointer=checkpointer,
)
def run_agent(thread_id: str, prompt: str):
"""
Run one agent turn for a specific conversation thread.
StateBackend files are separated by thread_id.
"""
config = {
"configurable": {
"thread_id": thread_id,
}
}
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": prompt,
}
]
},
config=config,
)
print("\n" + "=" * 70)
print(f"THREAD ID: {thread_id}")
print("=" * 70)
print(result["messages"][-1].content)
return result
# =========================================================
# TURN 1: Write a file into StateBackend
# =========================================================
run_agent(
thread_id="talha-learning-thread",
prompt="""
Use the write_file tool to create this file:
Path: /learning/python_notes.md
Content:
# Python StateBackend Notes
- StateBackend behaves like a temporary filesystem.
- Files belong to a LangGraph thread.
- The same thread can access files in later turns.
After writing the file:
1. Use ls to confirm the file exists.
2. Reply with a short confirmation.
""",
)
# =========================================================
# TURN 2: Read the file from the SAME thread
# =========================================================
run_agent(
thread_id="talha-learning-thread",
prompt="""
Use read_file to read /learning/python_notes.md.
Then answer these questions:
1. What is the first bullet point?
2. Why can you read this file now?
Do not recreate or rewrite the file.
""",
)
# =========================================================
# TURN 3: Try to access the file from a DIFFERENT thread
# =========================================================
run_agent(
thread_id="another-user-thread",
prompt="""
Use ls to inspect the filesystem.
Can you find /learning/python_notes.md?
Explain why or why not. Do not create any files.
""",
)
Understanding the Three Important Pieces
This example has three main building blocks:
| Component | Purpose |
|---|---|
StateBackend() |
Gives the agent a virtual, thread-scoped file workspace. |
InMemorySaver() |
Saves the agent’s LangGraph state between calls while the Python process is running. |
thread_id |
Identifies the conversation/session whose state should be loaded. |
In short:
thread_id tells LangGraph which saved state to load
↓
checkpointer loads that state
↓
StateBackend makes that thread's files available to the agent
LangGraph uses thread_id as the primary key for saving and retrieving checkpoints. Without a thread ID, a checkpointer cannot reliably load state from a previous invocation. (reference.langchain.com)
Step 1: Creating the Checkpointer
checkpointer = InMemorySaver()
A checkpointer is responsible for preserving the agent’s state between calls.
Without a checkpointer, this sequence would not work reliably:
Call 1 → agent writes a file
Call 2 → agent starts with no saved state
With a checkpointer:
Call 1 → agent writes a file → state is saved
Call 2 → saved state is loaded → file is available
InMemorySaver stores checkpoints in Python memory. It is useful for debugging, tutorials, and local experiments, but its contents disappear when the process ends. LangGraph recommends using a durable checkpointer, such as a database-backed option, for production use. (reference.langchain.com)
Step 2: Creating the Model
model = init_chat_model(
"openrouter:nvidia/nemotron-3-super-120b-a12b",
max_tokens=4096,
)
This code initializes an LLM through OpenRouter.
The max_tokens=4096 setting limits the maximum response size. This is useful because model providers may allow very large output limits by default, which can increase cost or exceed account limits.
You can replace this model with another supported chat model if needed. The StateBackend behavior does not depend on the specific model provider.
Step 3: Creating a Deep Agent with StateBackend
agent = create_deep_agent(
model=model,
backend=StateBackend(),
checkpointer=checkpointer,
)
This is where the agent receives:
- A language model
- A backend for file operations
- A checkpointer for preserving state
StateBackend is already the default backend in Deep Agents, so this would also work:
agent = create_deep_agent(
model=model,
checkpointer=checkpointer,
)
However, explicitly writing backend=StateBackend() is a good teaching choice because it makes the storage behavior visible.
Step 4: Why run_agent() Matters
The helper function accepts two values:
def run_agent(thread_id: str, prompt: str):
-
thread_id: identifies the session -
prompt: tells the agent what to do
The most important part is the LangGraph configuration:
config = {
"configurable": {
"thread_id": thread_id,
}
}
Every time you call the agent, this configuration tells LangGraph:
Load and continue the state associated with this thread.
Then the agent runs:
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": prompt,
}
]
},
config=config,
)
The config object is what connects one call to the state from an earlier call.
Turn 1: Writing a File
The first call uses this thread ID:
thread_id="talha-learning-thread"
The prompt tells the agent to create:
/learning/python_notes.md
with this content:
# Python StateBackend Notes
- StateBackend behaves like a temporary filesystem.
- Files belong to a LangGraph thread.
- The same thread can access files in later turns.
The agent should use its file tool to write the file, then use ls to verify that it exists.
Conceptually, the result is:
Thread: talha-learning-thread
/
└── learning/
└── python_notes.md
At the end of the turn, LangGraph saves the thread’s updated state through InMemorySaver.
Turn 2: Reading the Same File
The second call uses the same thread ID:
thread_id="talha-learning-thread"
This is the reason the agent can read the file created in Turn 1.
Turn 1
thread_id = talha-learning-thread
file created
↓
state saved by InMemorySaver
↓
Turn 2
thread_id = talha-learning-thread
same state loaded
file is available
The agent is asked to read:
/learning/python_notes.md
It should then answer:
What is the first bullet point?
StateBackend behaves like a temporary filesystem.Why can you read this file now?
Because the samethread_idwas used, so LangGraph restored the previously saved state for that thread.
This is not the same as writing to your computer’s actual disk. The file is stored in the agent’s state, not automatically in a local folder on your machine. StateBackend is intended for thread-scoped agent files, while FilesystemBackend is the option that accesses real files under a configured local directory. (docs.langchain.com)
Turn 3: Using a Different Thread
The third call changes the ID:
thread_id="another-user-thread"
This creates or loads a completely separate state.
Thread A: talha-learning-thread
├── /learning/python_notes.md
└── conversation history from Turns 1 and 2
Thread B: another-user-thread
├── no files from Thread A
└── separate conversation state
When the agent runs ls in the second thread, it should not find:
/learning/python_notes.md
Why?
Because StateBackend files are tied to a specific LangGraph thread. The second thread has its own state and cannot access Thread A’s files. This thread-level separation is a core behavior of StateBackend. (docs.langchain.com)
Expected Outcome
Your exact wording may differ because an LLM generates the final response, but the behavior should look like this:
| Turn | Thread ID | Expected Result |
|---|---|---|
| Turn 1 | talha-learning-thread |
The file is created and appears in ls. |
| Turn 2 | talha-learning-thread |
The agent reads the file and answers from its contents. |
| Turn 3 | another-user-thread |
The file is not found because this is a separate thread. |
The key observation is simple:
Same thread_id → same state → file is visible
Different thread_id → different state → file is not visible
StateBackend vs Local Files
A common beginner mistake is assuming StateBackend writes files to your laptop.
It does not.
Here is the difference:
| Feature | StateBackend |
FilesystemBackend |
|---|---|---|
| Storage location | LangGraph agent state | Your real local filesystem |
| Scope | Current thread | Configured local directory |
| Persists across turns | Yes, with a checkpointer | Yes, because files are on disk |
| Persists after Python program exits | Not with InMemorySaver
|
Usually yes |
| Best use case | Agent scratch files and intermediate work | Controlled local development workflows |
| Safety | Does not directly expose your whole disk | Requires careful security controls |
For temporary agent work, StateBackend is often a safer and simpler choice. If an agent needs true cross-thread durable storage, Deep Agents provides StoreBackend; if it needs real local files, FilesystemBackend can be used carefully. (docs.langchain.com)
Important Limitation: InMemorySaver Is Temporary
This line is convenient:
checkpointer = InMemorySaver()
But it has one important limitation:
Python script stops
↓
InMemorySaver data is lost
↓
Saved thread state and StateBackend files are gone
So if you run the script today, stop it, and run it again tomorrow, the previous file will not exist anymore.
For production applications, you would normally use a persistent checkpointer backed by a database or managed service.
Common Mistakes to Avoid
1. Changing the thread_id accidentally
This will create a separate workspace:
run_agent(
thread_id="new-thread",
prompt="Read /learning/python_notes.md",
)
Even though the file was created earlier, it will not be visible because the thread is different.
2. Expecting files after restarting the program
With InMemorySaver, this will not work:
Run script → create file
Stop script
Run script again → try to read file
The in-memory data was removed when the Python process stopped.
3. Treating thread_id as authentication
A thread_id is a state identifier, not a complete security system.
In a real multi-user application:
- Authenticate the user.
- Authorize access on the server.
- Generate non-guessable thread IDs.
- Associate each thread with its owner.
- Do not allow users to submit arbitrary thread IDs without validation.
Thread isolation is useful, but it should be part of a larger access-control design.
4. Assuming the agent will always follow instructions perfectly
The agent is driven by an LLM, so responses may vary slightly between runs.
For example, the agent may phrase its confirmation differently. However, the central behavior should remain the same:
- It writes the file in Turn 1.
- It reads it in Turn 2 using the same thread.
- It cannot access it from the different thread in Turn 3.
When Should You Use StateBackend?
Use StateBackend when your agent needs short-term, thread-specific files such as:
- Research notes
- Intermediate summaries
- Generated Markdown documents
- Large tool outputs
- Temporary plans
- Multi-step task artifacts
- Files shared between an agent and its subagents in the same workflow
A practical example:
Turn 1: Agent researches a topic and saves findings to /research/notes.md
Turn 2: Agent reads notes and writes a blog post
Turn 3: Agent reads the blog post and creates a social-media summary
As long as all turns use the same thread_id, the agent can continue using those files.
Final Takeaway
StateBackend gives a Deep Agent a virtual filesystem that belongs to its current LangGraph thread.
The behavior in this example can be summarized in one sentence:
A file written by an agent remains available in later turns only when the same
thread_idand saved LangGraph state are used.
StateBackend + Checkpointer + Same thread_id
=
Files available across agent turns
For local learning and experimentation, InMemorySaver is a great starting point. When you move to production, replace it with persistent storage and add proper user authentication and thread ownership checks.
Further Reading
- Deep Agents backend documentation explains the differences between
StateBackend,FilesystemBackend, andStoreBackend. (docs.langchain.com) - LangGraph persistence documentation explains threads, checkpoints, and state recovery. (docs.langchain.com)
Top comments (0)