Workspaces
Workspaces give your agent a typed I/O surface for files, shell commands, code execution, and snapshots — all auto-injected as tools the LLM can call. Pluggable providers back the surface with different storage and execution models.
Why workspaces
Without workspaces, every agent that needs to manipulate files or run code has to define its own bespoke tools. That means duplicated tool implementations, inconsistent semantics across agents, and no path to swap "in-memory for tests" with "real container for prod."
Workspaces solve that by:
- Decoupling capability from backing store. Declare what your agent needs (
fs,shell,code,snapshot); the framework injects matching tools and wires them to whichever provider you configure. - Auto-injecting LLM tools. A workspace named
boxwithfs: trueproducesworkspace__box__read_file,workspace__box__write_file,workspace__box__ls, etc., automatically. No bespoke tool code needed. - Surviving runtime boundaries. The framework persists serializable refs to your provider's storage so sessions resume cleanly across DO hibernation, Temporal replay, or process restarts.
The four built-in providers
| Provider | Backing | Modules | Use case |
|---|---|---|---|
| In-Memory | JavaScript Map | fs | Tests, dev, ephemeral agents. No persistence. |
| Local Bash | tmpdir + POSIX shell | fs, shell | Local development on POSIX systems. Not for production (no isolation). |
| Cloudflare Filestore | Durable Object SQLite + optional R2 | fs | Lightest CF option for durable file storage. No container, no cold start. |
| Cloudflare Sandbox | Workers Container (Firecracker microVM) | fs, shell, code, snapshot | Full Linux container for code execution. Real shell, Python/JS interpreter, R2-backed snapshots. |
See per-provider pages for setup, capabilities, and lifecycle details.
Decision matrix
| If you need... | Use |
|---|---|
| Tests / dev / no persistence | In-Memory |
| Local POSIX dev + real shell | Local Bash |
| Durable file storage on Cloudflare DO | Cloudflare Filestore |
| Code execution / shell on Cloudflare | Cloudflare Sandbox |
Quick start
The simplest possible workspace — in-memory, fs only, on the JS runtime:
import { defineAgent } from '@helix-agents/core';
import { JSAgentExecutor } from '@helix-agents/runtime-js';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { InMemoryWorkspaceProvider } from '@helix-agents/workspace-memory';
const agent = defineAgent({
name: 'file-writer',
llmConfig: { model: yourModel },
workspaces: {
notes: {
provider: { kind: 'in-memory' },
capabilities: { fs: true }, // → injects workspace__notes__read_file, write_file, etc.
},
},
});
const executor = new JSAgentExecutor(
new InMemoryStateStore(),
new InMemoryStreamManager(),
yourLLMAdapter,
{ workspaceProviders: new Map([['in-memory', new InMemoryWorkspaceProvider()]]) }
);
await executor.execute(agent, { message: 'Write a poem to /poem.txt' }, { sessionId: 'demo' });Three things going on:
workspaces.notesdeclares a workspace namednotes. The agent's LLM sees auto-injected tools prefixedworkspace__notes__*.provider: { kind: 'in-memory' }picks the provider. The discriminator (kind) matches the registered provider's id.workspaceProviderson the executor registers provider instances. The executor callsprovider.open(config, session)when the agent first uses a workspace tool.
Capability config
Capabilities are declared per-workspace. Each capability accepts either true (defaults) or an object with policy options:
workspaces: {
box: {
provider: { kind: 'cloudflare-sandbox' },
capabilities: {
fs: { maxFileSizeMb: 10 }, // policy-style
shell: { allowedCommands: ['ls', 'cat'] },
code: { languages: ['python'], isStateful: true },
snapshot: true,
},
},
},A few rules:
- A capability set to
true(or an object) → the framework auto-injects matching LLM tools. - A capability set to
false(or omitted) → no tools injected. The LLM literally cannot call them. - Capability config drives BOTH which tools get injected AND which policies apply at the tool layer (allowlists, max sizes, etc.). Provider configuration is separate (provider-side options live under
provider).
See per-module pages for full capability config schemas:
Auto-injected tools
For a workspace named box with fs: true, the LLM sees these tools (a subset based on the module):
workspace__box__read_file(path)workspace__box__write_file(path, content)workspace__box__edit_file(path, oldText, newText)workspace__box__ls(path)workspace__box__glob(pattern)workspace__box__grep(pattern, opts?)workspace__box__stat(path)workspace__box__mkdir(path, opts?)workspace__box__rm(path, opts?)
When shell: true is added: workspace__box__run(command, opts?).
When code: { languages, isStateful } is added: workspace__box__run_code(language, code). With isStateful: true, three more: create_code_context, run_in_code_context, delete_code_context.
When snapshot: true is added: workspace__box__snapshot() and workspace__box__restore(ref). If the provider implements branch?, workspace__box__branch(ref) too.
Lifecycle
A workspace's life cycle:
- Declared in the agent config (
defineAgent({ workspaces: { ... } })). - Opened lazily on first tool use — the framework calls
provider.open(config, session). - Used by the LLM via auto-injected tools, which dispatch through the runtime to the live
Workspaceinstance. - Refed — the framework persists a serializable
WorkspaceRefreturned byopen()so it can reattach later. - Resolved after a runtime boundary (DO hibernation, Temporal replay, executor restart) via
provider.resolve(ref). - Closed at session end via
workspace.close().
Different providers handle (1)–(6) differently — see per-provider pages.
Runnable example
The Workspaces Showcase example runs the same agent against all four providers via env-var dispatch. Single source of truth for "what does each provider feel like in code".
For a real-world integration story, see the Research Assistant (Cloudflare DO) example — a production-shape agent that adopts CloudflareFileStoreWorkspace to persist research notes durably. The example's README walks through a BEFORE/AFTER migration.
Next steps
- Pick a provider based on your runtime + persistence needs (decision matrix above).
- Read the per-provider page for setup specifics (wrangler config, DO bindings, Dockerfiles where applicable).
- Read per-module pages to understand the auto-injected tool surface and capability config options.
- Building your own provider? Start with Building a Provider.