DEV Community

Syeed Talha
Syeed Talha

Posted on

Understanding FilesystemBackend in LangChain's Deep Agents

In a previous example, we used StateBackend, where files belonged to a specific LangGraph conversation thread.

This time, we are using something different:

FilesystemBackend lets 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:

  1. The agent writes files to a real folder on disk.
  2. Those files remain after the Python program stops.
  3. thread_id does not isolate files when using local disk storage.
  4. CompositeBackend can 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 ─┘
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Before You Run the Code

Install the required packages:

pip install deepagents langchain langgraph python-dotenv
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The call to load_dotenv() loads environment variables from that file:

from dotenv import load_dotenv

load_dotenv()
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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,
    ),
)
Enter fullscreen mode Exit fullscreen mode

This tells the agent:

Use the agent_workspace folder as the location for real file operations.

When the agent receives this virtual path:

/notes/todo.md
Enter fullscreen mode Exit fullscreen mode

it maps to a real path similar to this:

your-project-folder/
└── agent_workspace/
    └── notes/
        └── todo.md
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

the final file could be:

C:\Users\Talha\deep-agents-demo\agent_workspace\notes\todo.md
Enter fullscreen mode Exit fullscreen mode

On macOS or Linux, it could look like:

/home/talha/deep-agents-demo/agent_workspace/notes/todo.md
Enter fullscreen mode Exit fullscreen mode

Why Use virtual_mode=True?

virtual_mode=True
Enter fullscreen mode Exit fullscreen mode

This is an important safety setting.

It lets the agent use clean virtual paths such as:

/notes/todo.md
Enter fullscreen mode Exit fullscreen mode

while mapping them underneath the configured root_dir.

It also blocks common path-escape attempts such as:

../../some-other-folder/file.txt
Enter fullscreen mode Exit fullscreen mode

or:

~/secret-file.txt
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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,
    ),
)
Enter fullscreen mode Exit fullscreen mode

Now workspace_dir becomes a complete OS path automatically.

For example:

C:\Users\Talha\deep-agents-demo\agent_workspace
Enter fullscreen mode Exit fullscreen mode

or:

/home/talha/deep-agents-demo/agent_workspace
Enter fullscreen mode Exit fullscreen mode

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}},
    )
Enter fullscreen mode Exit fullscreen mode

The thread_id is still passed to LangGraph:

config={"configurable": {"thread_id": thread_id}}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

with this content:

# To-do
- Buy groceries
- Finish the project
- Review pull requests
Enter fullscreen mode Exit fullscreen mode

Because the backend root directory is:

./agent_workspace
Enter fullscreen mode Exit fullscreen mode

the actual file becomes:

./agent_workspace/notes/todo.md
Enter fullscreen mode Exit fullscreen mode

The agent should then call ls to confirm that the file exists.

Conceptually, the filesystem now looks like this:

agent_workspace/
└── notes/
    └── todo.md
Enter fullscreen mode Exit fullscreen mode

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",
    ...
)
Enter fullscreen mode Exit fullscreen mode

The agent reads:

/notes/todo.md
Enter fullscreen mode Exit fullscreen mode

and should report:

  1. Buy groceries
  2. Finish the project
  3. 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
Enter fullscreen mode Exit fullscreen mode

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",
    ...
)
Enter fullscreen mode Exit fullscreen mode

The code uses a different thread ID:

demo-thread-2
Enter fullscreen mode Exit fullscreen mode

But the agent can still find and read:

/notes/todo.md
Enter fullscreen mode Exit fullscreen mode

Why?

Because both threads point to the same real directory:

./agent_workspace
Enter fullscreen mode Exit fullscreen mode

Here is the flow:

Thread: demo-thread-1
        │
        │ writes
        ▼
agent_workspace/notes/todo.md
        ▲
        │ reads
        │
Thread: demo-thread-2
Enter fullscreen mode Exit fullscreen mode

This means thread_id is not a filesystem security boundary when using FilesystemBackend.

If two agents use the same FilesystemBackend root 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
Enter fullscreen mode Exit fullscreen mode

Run them in Command Prompt:

dir agent_workspace\notes\
type agent_workspace\notes\todo.md
Enter fullscreen mode Exit fullscreen mode

Expected output:

# To-do
- Buy groceries
- Finish the project
- Review pull requests
Enter fullscreen mode Exit fullscreen mode

For PowerShell, you can use:

Get-ChildItem .\agent_workspace\notes\
Get-Content .\agent_workspace\notes\todo.md
Enter fullscreen mode Exit fullscreen mode

For macOS or Linux:

ls agent_workspace/notes/
cat agent_workspace/notes/todo.md
Enter fullscreen mode Exit fullscreen mode

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,
        ),
    },
)
Enter fullscreen mode Exit fullscreen mode

This is a hybrid approach.

It says:

  • Use StateBackend for 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
Enter fullscreen mode Exit fullscreen mode

And visually:

                         CompositeBackend
                                │
                 ┌──────────────┴──────────────┐
                 │                             │
      /workspace/... paths               All other paths
                 │                             │
                 ▼                             ▼
      FilesystemBackend                  StateBackend
       ./my_project/                  temporary state
Enter fullscreen mode Exit fullscreen mode

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,
    ),
},
Enter fullscreen mode Exit fullscreen mode

means that this agent path:

/workspace/src/hello.py
Enter fullscreen mode Exit fullscreen mode

will become a real file at:

./my_project/src/hello.py
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

After the agent finishes, your project folder should contain:

my_project/
└── src/
    └── hello.py
Enter fullscreen mode Exit fullscreen mode

The contents of hello.py should be:

print("hello from FilesystemBackend")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Only explicit project output goes to disk:

/workspace/...
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

Or on macOS/Linux:

FilesystemBackend(
    root_dir="/",
    virtual_mode=True,
)
Enter fullscreen mode Exit fullscreen mode

Instead, create a dedicated workspace:

FilesystemBackend(
    root_dir="./agent_workspace",
    virtual_mode=True,
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A safer project structure might look like:

my-app/
├── .env                    # Keep outside agent workspace
├── app/
├── agent_workspace/        # Agent can access this
└── run_agent.py
Enter fullscreen mode Exit fullscreen mode

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:

  • StateBackend for temporary per-thread files
  • StoreBackend for 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.”
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For simple local experiments:

FilesystemBackend(
    root_dir="./agent_workspace",
    virtual_mode=True,
)
Enter fullscreen mode Exit fullscreen mode

For more organized real-world projects:

CompositeBackend(
    default=StateBackend(),
    routes={
        "/workspace/": FilesystemBackend(
            root_dir="./my_project",
            virtual_mode=True,
        )
    }
)
Enter fullscreen mode Exit fullscreen mode

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)