Cloudflare Dynamic Worker Workspace
The CloudflareDynamicWorkerWorkspace is the lean, isolate-only Cloudflare provider — a lightweight, ephemeral JavaScript code runner backed by Cloudflare Dynamic Workers (Worker Loader isolates). It exposes a single capability — script — and nothing else (no fs, no shell, no code container, no snapshot, no DO, no R2). Use this when your agent only needs fast, throwaway JS compute and you don't want to pay for a container.
When to use
- Agents that need lightweight, ephemeral JS compute — transforms, calculations, sandboxed
eval-style evaluation. - Workloads where the full container (
cloudflare-sandbox) is overkill — a Worker Loader isolate is roughly ~100x cheaper than booting a Firecracker container and has no ~2–3s cold start. - Anywhere you want a sandboxed boundary for LLM-produced code without durable filesystem or state.
If your agent needs a real shell, durable files, persistence, R2-backed snapshots, or languages other than JavaScript, use the full Cloudflare Sandbox instead — or run BOTH tiers side-by-side via the sandbox provider's Script tier.
Capabilities supported
| Capability | Supported |
|---|---|
script | ✅ (Worker Loader isolate; JS-only, ephemeral) |
fs | ❌ |
shell | ❌ |
code | ❌ (use cloudflare-sandbox for the full interpreter) |
snapshot | ❌ |
This provider exposes ONLY script. Declaring any other capability against it injects tools the provider can't back — the usual WorkspaceFailedError at session start applies. See the error-model table on the workspaces overview.
Optional peer dependency
The Worker Loader types (WorkerLoader) come from @cloudflare/workers-types, an OPTIONAL peer dep on @helix-agents/runtime-cloudflare (already required by the DO runtime).
npm install -D @cloudflare/workers-typesUnlike the sandbox provider, this provider does NOT depend on @cloudflare/sandbox, so it ships in the main package barrel (no /sandbox subpath import):
import { CloudflareDynamicWorkerWorkspaceProvider } from '@helix-agents/runtime-cloudflare';Wrangler setup
The provider needs a worker_loaders binding. The Worker Loader binding is what lets the worker load and run ephemeral isolates at runtime; the worker reads it as env.LOADER.
// wrangler.jsonc
{
"worker_loaders": [{ "binding": "LOADER" }],
}No DO class, no container image, no R2 bucket — the isolate is loaded on demand and torn down by Cloudflare after each run.
Provider wiring
import {
createAgentServer,
CloudflareDynamicWorkerWorkspaceProvider,
} from '@helix-agents/runtime-cloudflare';
import type { WorkspaceProvider } from '@helix-agents/core';
interface Env {
AGENTS: DurableObjectNamespace;
LOADER: WorkerLoader;
OPENAI_API_KEY: string;
}
export const MyAgentServer = createAgentServer<Env>({
llmAdapter: (env) => /* ... */,
agents: registry,
workspaceProviders: (env) =>
new Map<string, WorkspaceProvider>([
[
'cloudflare-dynamic-worker',
new CloudflareDynamicWorkerWorkspaceProvider({ loader: env.LOADER }),
],
]),
});The provider takes { loader, logger? } — loader is the worker_loaders binding (env.LOADER); logger is the usual optional @helix-agents/core Logger (defaults to silent noopLogger).
The script capability + the workspace_script tool
Declare the script capability on the agent's workspace. The framework auto-injects a single tool, workspace_script, which the LLM calls with { code, language?, timeoutMs? }:
const agent = defineAgent({
name: 'calculator',
systemPrompt: 'Use workspace_script for quick JS computations.',
llmConfig: { model: /* ... */ },
workspace: {
provider: { kind: 'cloudflare-dynamic-worker' },
capabilities: { script: { network: 'off', maxDurationMs: 5000 } },
},
});Each workspace_script call loads a FRESH isolate (loader.get(null, ...)), runs the supplied code as an async-function body (so a top-level return yields the result), captures console.* output as stdout/stderr, and returns a RunCodeResult (outputs + exitCode). The isolate is the security boundary, so a syntax error surfaces as a run failure — the code cannot escape the worker. Cloudflare tears the isolate down after the run; nothing persists.
The script capability config (ScriptCapConfig) accepts:
network?: 'off' | 'allow'— outbound network for executed code. Default'off'(all egress blocked).maxDurationMs?: number— wall-clock cap (ms) per run. The LLM-suppliedtimeoutMson a singleworkspace_scriptcall overrides it for that call.languages?: readonly string[]— languages advertised by the runner. v1 is JavaScript-only; defaults to['javascript'].
The capability config above is honored by the provider and takes precedence over the provider/kind-level defaults: when both capabilities.script.network (or .maxDurationMs) and the matching provider-config field are set, the capabilities.script value wins. This is the more-specific declaration, so the recommended pattern is to put network / maxDurationMs on capabilities.script. (compatibilityDate is the exception — it has no ScriptCapConfig field and lives on the provider config only; see Provider config vs capability config below.)
v1 limits
- JavaScript only. The runner advertises
['javascript']; a non-javascriptlanguagereturns a run error pointing atworkspace_run_codefor other languages. WebAssembly is available in-isolate (you can instantiate Wasm modules from JS). - Stateless. A fresh isolate per
runCodecall — no variables, no files, no state carry across calls. (Contrast with the sandboxcodeinterpreter's optionalcodeStateful: truecontexts.) - Ephemeral
/tmpscratch only. No durable filesystem; any scratch the isolate writes is discarded when it's torn down. - Network off by default. Outbound egress is blocked unless you opt in with
script: { network: 'allow' }. v1 has no allowlist — it's all-or-nothing.
Python, Durable Object persistence, a network allowlist, and warm-isolate caching are deferred (see the changeset / spec).
Lifecycle
open()— always exposes exactly thescriptcapability. It merges the effective config (capabilities.script.network/.maxDurationMstake precedence over the provider-config fallbacks;compatibilityDatecomes from the provider config), constructs aWorkerLoaderScriptRunnerover the provider'sloaderbinding, and stamps aWorkspaceRefcarrying that merged config (network,maxDurationMs,compatibilityDate). The merge happens here becauseresolve()receives no declared capabilities — the effective config must round-trip through the ref. There is no live resource to attach to — isolates are created per call.resolve()— trivially reconstructs the runner from the ref payload (validated by a strict Zod schema). Because isolates are stateless/ephemeral, there's no reattach step; resume is just "rebuild the runner from config."close()— a no-op. Isolates are torn down by Cloudflare; there is nothing to close.
Using the workspace from a custom tool
import { defineTool, assertWorkspaceModule } from '@helix-agents/core';
import { z } from 'zod';
const computeChecksum = defineTool({
name: 'compute_checksum',
parameters: z.object({ input: z.string() }),
execute: async (input, ctx) => {
const ws = await ctx.workspaces!.get();
const script = assertWorkspaceModule(ws, 'script');
const result = await script.runCode(
'javascript',
`const data = ${JSON.stringify(input.input)};
let h = 0;
for (const c of data) h = (h * 31 + c.charCodeAt(0)) | 0;
return h;`
);
return { exitCode: result.exitCode, outputs: result.outputs };
},
});See the shared pattern on the overview page — await on get() is required, and assertWorkspaceModule(ws, 'script') produces a typed WorkspaceFailedError naming the missing capability if the agent forgot to declare it.
Config example
agent.workspace = {
// compatibilityDate is a PROVIDER-config option — it has no ScriptCapConfig
// field, so it lives here, not on capabilities.script.
provider: { kind: 'cloudflare-dynamic-worker', compatibilityDate: '2025-09-01' },
// network + maxDurationMs are preferred on capabilities.script (honored, and
// they take precedence over any provider-level fallback defaults).
capabilities: { script: { network: 'off', maxDurationMs: 5000 } },
};Provider config vs capability config
There are three places a setting can live; each option belongs to exactly one of them:
| Setting | Where it lives | Notes |
|---|---|---|
network | capabilities.script.network (preferred) or provider config | capabilities.script wins when both are set. Default 'off' (all egress blocked). |
maxDurationMs | capabilities.script.maxDurationMs (preferred) or provider config | capabilities.script wins when both are set. |
compatibilityDate | provider config only | No ScriptCapConfig field. Sets the Worker compatibility date of the loaded isolate. Defaults to the runner's built-in date. |
loader | constructor option (new ...Provider({ loader, logger? })) | The worker_loaders binding. An infrastructure concern, not per-workspace config — never on provider or capabilities. |
The CloudflareDynamicWorkerWorkspaceConfig (the provider object) shape is { kind, network?, maxDurationMs?, compatibilityDate? }. The network / maxDurationMs fields there are fallback defaults — if you also declare them on capabilities.script, the capability values take precedence.
Limitations
- Workflows runtime not supported. Workspaces require the DO runtime (
createAgentServer); the Workflows / Temporal / DBOS runtimes fail fast at agent registration when workspaces are declared. See the workspaces overview — runtime parity. - JavaScript only (v1). For Python or a full toolchain, use the Cloudflare Sandbox container.
- No durable state. Each run is a fresh isolate; nothing persists across calls or sessions. For durable files use Cloudflare Filestore or the sandbox container.