File system
Last updated Oct 23, 2026
gives an agent a persistent working directory backed by SQLite, along with the standard set of tools for working with the file system, including read, write, edit, and grep. An optional exec tool lets the agent manipulate the file system and run tools programmatically.
Use a file system when an agent needs to:
- Keep notes, drafts, plans, or generated files across turns and restarts.
- Read, search, and edit text files with tools built for a model.
- Write and run JavaScript against files, without starting a container.
- Provide skills, source files, and other external documents to the agent without filling its context window.
How it works
A Workspace lives inside a Durable Object and keeps every file in the SQLite storage of that object. Files remain after the Durable Object is evicted or restarted, with no external storage to configure.
workspace.fs reads, writes, lists, searches, and deletes files. workspace.runtime runs code against the same files through an execution backend. Computer includes a JavaScript runtime, as well as an experimental container-based runtime for running shell commands.
Install
npmyarnpnpmbun
npm i @cloudflare/computer zod
yarn add @cloudflare/computer zod
pnpm add @cloudflare/computer zod
bun add @cloudflare/computer zod
Every tool entry point requires zod. What else you need depends on how your agent calls tools:
- Think includes the workspace tools. Install
@cloudflare/computeralongside it. - AI SDK ↗︎ tools also require
ai. - Pi and OpenCode use the Pi AI tools, which need nothing beyond
zod.
Your Worker needs the nodejs_compat compatibility flag.
Add a workspace to a Durable Object
Create a Workspace in your Durable Object class and pass it the storage object. The file system is ready to use immediately.
import { Workspace } from "@cloudflare/computer";
import { DurableObject } from "cloudflare:workers";
export class NotesAgent extends DurableObject {
workspace = new Workspace({ storage: this.ctx.storage });
async addTodo(item) {
const path = "/workspace/notes/todo.md";
await this.workspace.fs.mkdir("/workspace/notes", { recursive: true });
let todo = "";
try {
todo = await this.workspace.fs.readFile(path, "utf8");
} catch (error) {
// A missing file means there are no to-do items yet.
if (error.code !== "ENOENT") throw error;
}
todo += `- [ ] ${item}\n`;
await this.workspace.fs.writeFile(path, todo);
return todo;
}
}
export default {
async fetch(request, env) {
const agent = env.NotesAgent.getByName("user-123");
return new Response(await agent.addTodo("Ship it"));
},
};
import { Workspace } from "@cloudflare/computer";
import { DurableObject } from "cloudflare:workers";
export class NotesAgent extends DurableObject<Env> {
workspace = new Workspace({ storage: this.ctx.storage });
async addTodo(item: string) {
const path = "/workspace/notes/todo.md";
await this.workspace.fs.mkdir("/workspace/notes", { recursive: true });
let todo = "";
try {
todo = await this.workspace.fs.readFile(path, "utf8");
} catch (error) {
// A missing file means there are no to-do items yet.
if ((error as { code?: string }).code !== "ENOENT") throw error;
}
todo += `- [ ] ${item}\n`;
await this.workspace.fs.writeFile(path, todo);
return todo;
}
}
export default {
async fetch(request: Request, env: Env) {
const agent = env.NotesAgent.getByName("user-123");
return new Response(await agent.addTodo("Ship it"));
},
} satisfies ExportedHandler<Env>;
Configure the Durable Object binding and a SQLite migration in your Wrangler configuration:
{
// Set this to today's date
"compatibility_date": "2026-10-27",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"class_name": "NotesAgent",
"name": "NotesAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["NotesAgent"]
}
]
}
# Set this to today's date
compatibility_date = "2026-10-27"
compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]
class_name = "NotesAgent"
name = "NotesAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "NotesAgent" ]
Work with files
The workspace.fs object provides common methods for working with the file system. It accepts string or binary Uint8Array content. Use a ReadableStream to read or write large files.
const { fs } = this.workspace;
// Write a string, bytes, or a stream.
await fs.writeFile("/workspace/data/blob.bin", new Uint8Array([1, 2, 3]));
await fs.writeFile("/workspace/uploads/report.csv", request.body);
// Read a file as text, or as a stream when you omit the encoding.
const text = await fs.readFile("/workspace/uploads/report.csv", "utf8");
const stream = await fs.readFile("/workspace/uploads/report.csv");
// List, search, and remove.
for (const entry of await fs.readdir("/workspace/notes")) {
console.log(entry.isDirectory ? `d ${entry.name}` : `f ${entry.name}`);
}
const matches = await fs.grep("TODO", "/workspace", { ignoreCase: true });
await fs.rm("/workspace/notes", { recursive: true });
const { fs } = this.workspace;
// Write a string, bytes, or a stream.
await fs.writeFile("/workspace/data/blob.bin", new Uint8Array([1, 2, 3]));
await fs.writeFile("/workspace/uploads/report.csv", request.body!);
// Read a file as text, or as a stream when you omit the encoding.
const text = await fs.readFile("/workspace/uploads/report.csv", "utf8");
const stream = await fs.readFile("/workspace/uploads/report.csv");
// List, search, and remove.
for (const entry of await fs.readdir("/workspace/notes")) {
console.log(entry.isDirectory ? `d ${entry.name}` : `f ${entry.name}`);
}
const matches = await fs.grep("TODO", "/workspace", { ignoreCase: true });
await fs.rm("/workspace/notes", { recursive: true });
For every file operation, refer to workspace.fs in the API reference.
Give the model file tools
Computer builds one set of file tools for a workspace, with adapters for different agent libraries:
| Tool | What it does |
|---|---|
read |
Reads a text file by line, or passes an image or PDF to a model that accepts it. |
ls |
Lists one directory, page by page. |
find |
Finds paths with *, **, and ? globs. |
grep |
Searches file contents with a regular expression or a fixed string. |
write |
Writes a whole file. |
edit |
Replaces targeted text in a file and returns a diff. |
delete |
Deletes a file or directory. |
exec |
Runs code on an execution backend. Present only when the workspace has one. |
Harnesses
Think adds these tools to every turn with no configuration. The Pi and OpenCode harnesses take the Pi AI tools through a small adapter. Any other agent built on the AI SDK can use createAITools().
Think
Every Think agent has a Computer Workspace at this.workspace, and Think adds the workspace tools to every turn. To change the workspace, override workspace. A custom tool from getTools() with the same name as a workspace tool replaces it.
For more information, refer to Built-in workspace tools.
AI SDK
createAITools() from @cloudflare/computer/tools/ai-sdk returns an AI SDK ToolSet for a workspace. Pass it to generateText(), streamText(), or the getTools() method of an agent.
import { Workspace } from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { generateText, stepCountIs } from "ai";
import { DurableObject } from "cloudflare:workers";
import { createAI } from "agents/models/ai-sdk";
export class WriterAgent extends DurableObject {
workspace = new Workspace({ storage: this.ctx.storage });
async write(prompt) {
const { text } = await generateText({
model: createAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.6"),
system:
"You are a writing assistant. Keep drafts and notes as files under /workspace.",
prompt,
tools: createAITools({
workspace: this.workspace,
read: { maxBytes: 32 * 1024, maxLines: 800 },
}),
stopWhen: stepCountIs(20),
});
return text;
}
}
import { Workspace } from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { generateText, stepCountIs } from "ai";
import { DurableObject } from "cloudflare:workers";
import { createAI } from "agents/models/ai-sdk";
export class WriterAgent extends DurableObject<Env> {
workspace = new Workspace({ storage: this.ctx.storage });
async write(prompt: string) {
const { text } = await generateText({
model: createAI({ binding: this.env.AI })(
"@cf/moonshotai/kimi-k2.6",
),
system:
"You are a writing assistant. Keep drafts and notes as files under /workspace.",
prompt,
tools: createAITools({
workspace: this.workspace,
read: { maxBytes: 32 * 1024, maxLines: 800 },
}),
stopWhen: stepCountIs(20),
});
return text;
}
}
Pi
createPiTools() from @cloudflare/computer/tools/pi-ai returns Pi AI tool declarations and one execute function that runs a call by name. Pi Durable runs the agent loop itself and expects each tool to carry its own execute, so pair them up as ToolRegistration objects:
src/workspace-tools.jsjs
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
// Running these again after an eviction is harmless.
const REPLAY_SAFE = new Set(["read", "ls", "find", "grep", "write", "delete"]);
export function workspaceTools(workspace) {
const { tools, execute } = createPiTools({ workspace });
return tools.map((tool) => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
replay: REPLAY_SAFE.has(tool.name) ? "safe" : "unsafe",
async execute(args, api, context) {
const { content, isError } = await execute(
{ id: api.callId, name: tool.name, arguments: args },
{ abortSignal: context.abortSignal },
);
return { output: content, isError };
},
}));
}
src/workspace-tools.tsts
import type { Workspace } from "@cloudflare/computer";
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
import type { ToolRegistration } from "@earendil-works/pi-durable";
// Running these again after an eviction is harmless.
const REPLAY_SAFE = new Set(["read", "ls", "find", "grep", "write", "delete"]);
export function workspaceTools(workspace: Workspace): ToolRegistration[] {
const { tools, execute } = createPiTools({ workspace });
return tools.map((tool): ToolRegistration => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
replay: REPLAY_SAFE.has(tool.name) ? "safe" : "unsafe",
async execute(args, api, context) {
const { content, isError } = await execute(
{ id: api.callId, name: tool.name, arguments: args },
{ abortSignal: context.abortSignal },
);
return { output: content, isError };
},
}));
}
Install the tools in an extension inside the harness factory, before Harness.open():
this.registry.install({
name: "workspace",
tools: workspaceTools(this.workspace),
});
this.registry.install({
name: "workspace",
tools: workspaceTools(this.workspace),
});
execute validates the arguments against the schema of the tool and reports failures as isError results, so the model can retry.
OpenCode
OpenCode receives tools through plugins. The following plugin replaces the OpenCode tools that need a local file system with the workspace tools:
src/workspace-plugin.jsjs
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
// OpenCode tools that need a local file system or process.
const LOCAL_TOOLS = ["read", "write", "edit", "patch", "glob", "grep", "shell"];
export function workspacePlugin(workspace) {
const { tools, execute } = createPiTools({ workspace });
return {
id: "workspace",
async setup(ctx) {
await ctx.tool.transform((editor) => {
for (const id of LOCAL_TOOLS) editor.remove(id);
for (const tool of tools) {
editor.add({
name: tool.name,
description: tool.description,
input: tool.parameters,
// Offer each tool by name instead of through OpenCode's code mode.
options: { codemode: false },
async execute(input, context) {
const { content, isError } = await execute(
{ id: context.id, name: tool.name, arguments: input },
{ abortSignal: context.signal },
);
const text = content
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n");
if (isError) throw new Error(text || `${tool.name} failed`);
return { content: text };
},
});
}
});
},
};
}
