In a previous example, we used StateBackend, where files belonged to a specific LangGraph conversation thread.
This time, we are using something different:
FilesystemBackendlets a Deep Agent read and write real files on your machine.
That means if the agent creates a Markdown file, Python file, or project folder, it remains on your disk even after the script finishes.
This is useful for local coding assistants, document-generation tools, and development workflows. But because the agent can modify real files, it must be used carefully.
What This Example Demonstrates
The code shows four important behaviors:
- The agent writes files to a real folder on disk.
- Those files remain after the Python program stops.
-
thread_iddoes not isolate files when using local disk storage. -
CompositeBackendcan keep temporary agent artifacts separate from your real project files.
Here is the high-level comparison:
StateBackend
────────────
Thread A → temporary files for Thread A
Thread B → separate temporary files for Thread B
FilesystemBackend
─────────────────
Thread A ─┐
├── same real folder on your computer
Thread B ─┘
With FilesystemBackend, files are not stored inside LangGraph state. They are regular operating-system files under a configured directory. (docs.langchain.com)
The Full Example
"""
Demonstrates Deep Agents' FilesystemBackend (real files on disk).
What you will observe:
1. The agent writes real files to disk under a root_dir.
2. Files persist after the Python process ends — inspect them.
3. Thread isolation does NOT apply (files are real OS files).
4. CompositeBackend keeps internal agent artifacts out of your project.
"""
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, FilesystemBackend, StateBackend
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
load_dotenv()
model = init_chat_model(
"openrouter:nvidia/nemotron-3-super-120b-a12b",
max_tokens=4096,
)
# ---------------------------------------------------------
# 1. FilesystemBackend (standalone) — writes under a root_dir
# ---------------------------------------------------------
# Files go to real disk under ./agent_workspace/ on your machine.
agent = create_deep_agent(
model=model,
backend=FilesystemBackend(
root_dir="./agent_workspace",
virtual_mode=True,
),
)
def run(thread_id: str, prompt: str):
result = agent.invoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"configurable": {"thread_id": thread_id}},
)
print("\n" + "=" * 70)
print(f"THREAD: {thread_id}")
print("=" * 70)
print(result["messages"][-1].content)
# ---------------------------------------------------------
# Turn 1 — write a real file
# ---------------------------------------------------------
run(
"demo-thread-1",
"""
Use the write_file tool to create a file:
Path: /notes/todo.md
Content:
# To-do
- Buy groceries
- Finish the project
- Review pull requests
After writing:
1. Use ls to confirm it exists.
2. Tell me the absolute OS path where this file was written.
""",
)
# ---------------------------------------------------------
# Turn 2 — same thread reads it back
# ---------------------------------------------------------
run(
"demo-thread-1",
"""
Use read_file to read /notes/todo.md.
Then list the three to-do items.
""",
)
# ---------------------------------------------------------
# Turn 3 — different thread ALSO sees the file
# ---------------------------------------------------------
# Unlike StateBackend, files are real OS files — they are NOT
# isolated by thread_id. Any thread can read any file.
run(
"demo-thread-2",
"""
Use ls to inspect the filesystem.
Can you find /notes/todo.md? Read it and report its contents.
""",
)
print("\n" + "=" * 70)
print("Files written to: ./agent_workspace/ -- check with:")
print(" dir agent_workspace\\notes\\")
print(" type agent_workspace\\notes\\todo.md")
print("=" * 70)
# ---------------------------------------------------------
# 2. CompositeBackend (recommended for real projects)
# ---------------------------------------------------------
# Internal agent artifacts (large tool results, conversation
# history) go to ephemeral StateBackend. Only files under
# /workspace/ hit real disk.
print("\n" + "=" * 70)
print("COMPOSITE BACKEND (recommended pattern)")
print("=" * 70)
agent2 = create_deep_agent(
model=model,
backend=CompositeBackend(
default=StateBackend(),
routes={
"/workspace/": FilesystemBackend(
root_dir="./my_project",
virtual_mode=True,
),
},
),
)
result = agent2.invoke(
{
"messages": [
{
"role": "user",
"content": (
"Write a file at path /workspace/src/hello.py "
"with content: print('hello from FilesystemBackend'). "
"Then confirm it exists via ls."
),
}
]
},
config={"configurable": {"thread_id": "composite-demo"}},
)
print(result["messages"][-1].content)
print("\nFile written to: ./my_project/src/hello.py")
Before You Run the Code
Install the required packages:
pip install deepagents langchain langgraph python-dotenv
You also need your model provider credentials configured. Since this example uses OpenRouter, place your API key in a .env file:
OPENROUTER_API_KEY=your_api_key_here
The call to load_dotenv() loads environment variables from that file:
from dotenv import load_dotenv
load_dotenv()
Do not commit .env files containing real API keys to GitHub.
The Important Difference: Real Disk vs Agent State
Before studying the code, understand this difference.
| Feature | StateBackend |
FilesystemBackend |
|---|---|---|
| Where files live | LangGraph state | Your computer’s filesystem |
| Survives script restart | No, with InMemorySaver
|
Yes |
Isolated by thread_id
|
Yes | No |
| Good for | Temporary agent artifacts | Local projects and durable output |
| Risk level | Lower | Higher |
| Can affect real project files | No | Yes |
FilesystemBackend reads and writes directly to the local filesystem. It is intended for controlled local development or carefully configured CI/CD workflows—not directly exposed web applications or multi-user APIs. (reference.langchain.com)
Step 1: Creating the Model
model = init_chat_model(
"openrouter:nvidia/nemotron-3-super-120b-a12b",
max_tokens=4096,
)
This creates the language model used by the Deep Agent.
The max_tokens=4096 argument limits the size of generated responses. This is practical when using paid APIs because it helps control token usage and cost.
You can use another supported model later. The storage behavior explained in this article comes from the backend configuration, not from the model itself.
Step 2: Creating a FilesystemBackend
Here is the most important part of the first agent:
agent = create_deep_agent(
model=model,
backend=FilesystemBackend(
root_dir="./agent_workspace",
virtual_mode=True,
),
)
This tells the agent:
Use the
agent_workspacefolder as the location for real file operations.
When the agent receives this virtual path:
/notes/todo.md
it maps to a real path similar to this:
your-project-folder/
└── agent_workspace/
└── notes/
└── todo.md
The exact full path depends on where you run your Python script.
For example, if your project is located at:
C:\Users\Talha\deep-agents-demo
the final file could be:
C:\Users\Talha\deep-agents-demo\agent_workspace\notes\todo.md
On macOS or Linux, it could look like:
/home/talha/deep-agents-demo/agent_workspace/notes/todo.md
Why Use virtual_mode=True?
virtual_mode=True
This is an important safety setting.
It lets the agent use clean virtual paths such as:
/notes/todo.md
while mapping them underneath the configured root_dir.
It also blocks common path-escape attempts such as:
../../some-other-folder/file.txt
or:
~/secret-file.txt
However, this is not full sandboxing. It provides path-based restrictions, but it does not isolate the Python process like a container or remote sandbox would. The official Deep Agents documentation recommends using virtual_mode=True together with a configured root directory. (reference.langchain.com)
A Small Improvement: Use an Absolute root_dir
For beginner projects, this works:
root_dir="./agent_workspace"
But using an absolute path makes your code more predictable, especially if you run the script from different directories.
Here is a slightly safer version:
from pathlib import Path
workspace_dir = Path("./agent_workspace").resolve()
agent = create_deep_agent(
model=model,
backend=FilesystemBackend(
root_dir=workspace_dir,
virtual_mode=True,
),
)
Now workspace_dir becomes a complete OS path automatically.
For example:
C:\Users\Talha\deep-agents-demo\agent_workspace
or:
/home/talha/deep-agents-demo/agent_workspace
Step 3: The run() Helper Function
The helper function invokes the agent:
def run(thread_id: str, prompt: str):
result = agent.invoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"configurable": {"thread_id": thread_id}},
)
The thread_id is still passed to LangGraph:
config={"configurable": {"thread_id": thread_id}}
But with FilesystemBackend, the file itself does not belong to the thread.
This is a crucial distinction:
thread_id controls conversation/checkpoint state
root_dir controls real file location
The thread can affect conversation memory, but a file written to disk is visible to anything that has permission to access that disk location.
Turn 1: Writing a Real Markdown File
The first prompt asks the agent to create:
/notes/todo.md
with this content:
# To-do
- Buy groceries
- Finish the project
- Review pull requests
Because the backend root directory is:
./agent_workspace
the actual file becomes:
./agent_workspace/notes/todo.md
The agent should then call ls to confirm that the file exists.
Conceptually, the filesystem now looks like this:
agent_workspace/
└── notes/
└── todo.md
Unlike StateBackend, this file is not temporary agent state. It is a real file created on your computer.
Turn 2: Reading the File Again
The second call uses the same thread:
run(
"demo-thread-1",
...
)
The agent reads:
/notes/todo.md
and should report:
- Buy groceries
- Finish the project
- Review pull requests
At this point, it may look similar to the StateBackend example. But the reason the file is available is different.
With StateBackend, the same thread ID matters because files are stored in thread state.
With FilesystemBackend, the file is available because it physically exists in:
./agent_workspace/notes/todo.md
The agent could read it even after restarting the Python script, as long as the file is still on disk.
Turn 3: A Different Thread Can Read the Same File
Now notice this call:
run(
"demo-thread-2",
...
)
The code uses a different thread ID:
demo-thread-2
But the agent can still find and read:
/notes/todo.md
Why?
Because both threads point to the same real directory:
./agent_workspace
Here is the flow:
Thread: demo-thread-1
│
│ writes
▼
agent_workspace/notes/todo.md
▲
│ reads
│
Thread: demo-thread-2
This means thread_id is not a filesystem security boundary when using FilesystemBackend.
If two agents use the same
FilesystemBackendroot directory, they can access the same files—subject to operating-system permissions and the backend configuration.
Verifying the File Yourself
Your code prints these Windows commands:
dir agent_workspace\notes\
type agent_workspace\notes\todo.md
Run them in Command Prompt:
dir agent_workspace\notes\
type agent_workspace\notes\todo.md
Expected output:
# To-do
- Buy groceries
- Finish the project
- Review pull requests
For PowerShell, you can use:
Get-ChildItem .\agent_workspace\notes\
Get-Content .\agent_workspace\notes\todo.md
For macOS or Linux:
ls agent_workspace/notes/
cat agent_workspace/notes/todo.md
This is a helpful test because it proves that the file exists independently of the agent and the Python process.
Why CompositeBackend Is Better for Real Projects
The second part of the code introduces this:
CompositeBackend(
default=StateBackend(),
routes={
"/workspace/": FilesystemBackend(
root_dir="./my_project",
virtual_mode=True,
),
},
)
This is a hybrid approach.
It says:
- Use
StateBackendfor most agent files. - Use real disk only for paths beginning with
/workspace/.
Here is the routing idea:
Agent path Backend used
────────────────────────────────────────────────────
/workspace/src/hello.py FilesystemBackend
/tmp/tool-output.txt StateBackend
/internal/agent-notes.md StateBackend
And visually:
CompositeBackend
│
┌──────────────┴──────────────┐
│ │
/workspace/... paths All other paths
│ │
▼ ▼
FilesystemBackend StateBackend
./my_project/ temporary state
CompositeBackend routes file operations by path prefix. A matching route sends the operation to that backend; paths that do not match a route use the default backend. (reference.langchain.com)
Understanding the /workspace/ Route
This configuration:
routes={
"/workspace/": FilesystemBackend(
root_dir="./my_project",
virtual_mode=True,
),
},
means that this agent path:
/workspace/src/hello.py
will become a real file at:
./my_project/src/hello.py
The agent prompt says:
"Write a file at path /workspace/src/hello.py "
"with content: print('hello from FilesystemBackend'). "
"Then confirm it exists via ls."
After the agent finishes, your project folder should contain:
my_project/
└── src/
└── hello.py
The contents of hello.py should be:
print("hello from FilesystemBackend")
Why Keep Internal Files in StateBackend?
Deep Agents can create temporary artifacts while working, such as:
- Large tool results
- Intermediate research notes
- Planning files
- Conversation-related artifacts
- Temporary generated content
You may not want these files to appear in your actual source-code project.
That is why this pattern is useful:
default=StateBackend()
Only explicit project output goes to disk:
/workspace/...
Everything else remains temporary and isolated in agent state.
This creates a cleaner project directory and reduces the chance that internal agent artifacts are mixed with your application files.
FilesystemBackend vs CompositeBackend
| Question | Standalone FilesystemBackend
|
CompositeBackend |
|---|---|---|
| Where do files go? | One real disk folder | Different locations based on path |
| Are all agent files written to disk? | Usually yes | Only selected routes |
| Can temporary artifacts stay ephemeral? | No, not by default | Yes |
| Best for | Small local experiments | Real development workflows |
| Project-folder cleanliness | Can become messy | Easier to control |
| Configuration complexity | Simple | Slightly more advanced |
For a quick experiment, use FilesystemBackend.
For a project where you want the agent to create real source files but keep its internal work temporary, use CompositeBackend.
Important Safety Warning
FilesystemBackend is powerful because it gives an AI agent access to real files.
That also makes it risky.
The official documentation warns that agents using this backend may be able to read accessible secrets such as .env files, API keys, or credentials, and that file changes are permanent. It is intended for controlled local development and carefully managed CI/CD environments—not public web servers, multi-tenant apps, or untrusted user input. (reference.langchain.com)
Avoid These Mistakes
1. Do not point the agent at your entire computer
Avoid this:
FilesystemBackend(
root_dir="C:/",
virtual_mode=True,
)
Or on macOS/Linux:
FilesystemBackend(
root_dir="/",
virtual_mode=True,
)
Instead, create a dedicated workspace:
FilesystemBackend(
root_dir="./agent_workspace",
virtual_mode=True,
)
2. Do not expose secrets inside the workspace
Avoid placing files like these inside the folder available to the agent:
.env
credentials.json
private_key.pem
config.production.json
A safer project structure might look like:
my-app/
├── .env # Keep outside agent workspace
├── app/
├── agent_workspace/ # Agent can access this
└── run_agent.py
3. Do not use it directly in a public API
If users can send prompts to an agent through a website, avoid giving that agent direct access to your host machine’s filesystem.
For production systems, prefer:
-
StateBackendfor temporary per-thread files -
StoreBackendfor durable managed storage - A sandbox backend for isolated execution environments
Deep Agents specifically recommends safer state, store, or sandbox alternatives for web servers and HTTP APIs. (reference.langchain.com)
4. Do not treat thread_id as access control
This is unsafe thinking:
“My files are secure because each user has a different thread_id.”
That is true for StateBackend, but not for a shared FilesystemBackend.
If different users, agents, or processes share the same disk folder, they may see the same files.
Use proper authentication, authorization, per-user directories, and operating-system permissions in real applications.
A Recommended Project Structure
For experimenting safely, use a layout like this:
deepagents-filesystem-demo/
│
├── .env
├── filesystem_demo.py
│
├── agent_workspace/
│ └── notes/
│ └── todo.md
│
└── my_project/
└── src/
└── hello.py
You can also add these generated folders to .gitignore:
# Agent-generated workspace files
agent_workspace/
# Add this only if you do not want generated project files in Git
# my_project/
# Never commit environment secrets
.env
Final Takeaway
FilesystemBackend allows a Deep Agent to work with files that truly exist on your machine.
The core idea is:
StateBackend
→ files are temporary and tied to a LangGraph thread
FilesystemBackend
→ files are real, persistent, and shared through the operating system
For simple local experiments:
FilesystemBackend(
root_dir="./agent_workspace",
virtual_mode=True,
)
For more organized real-world projects:
CompositeBackend(
default=StateBackend(),
routes={
"/workspace/": FilesystemBackend(
root_dir="./my_project",
virtual_mode=True,
)
}
)
This hybrid approach gives you the best of both worlds:
- real project files where you want them;
- temporary agent artifacts kept out of your project folder.
Top comments (0)