Skip to content

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

CapabilitySupported
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).

bash
npm install -D @cloudflare/workers-types

Unlike the sandbox provider, this provider does NOT depend on @cloudflare/sandbox, so it ships in the main package barrel (no /sandbox subpath import):

typescript
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.

jsonc
// 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

typescript
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? }:

typescript
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-supplied timeoutMs on a single workspace_script call 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-javascript language returns a run error pointing at workspace_run_code for other languages. WebAssembly is available in-isolate (you can instantiate Wasm modules from JS).
  • Stateless. A fresh isolate per runCode call — no variables, no files, no state carry across calls. (Contrast with the sandbox code interpreter's optional codeStateful: true contexts.)
  • Ephemeral /tmp scratch 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 the script capability. It merges the effective config (capabilities.script.network / .maxDurationMs take precedence over the provider-config fallbacks; compatibilityDate comes from the provider config), constructs a WorkerLoaderScriptRunner over the provider's loader binding, and stamps a WorkspaceRef carrying that merged config (network, maxDurationMs, compatibilityDate). The merge happens here because resolve() 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

typescript
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 pageawait 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

typescript
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:

SettingWhere it livesNotes
networkcapabilities.script.network (preferred) or provider configcapabilities.script wins when both are set. Default 'off' (all egress blocked).
maxDurationMscapabilities.script.maxDurationMs (preferred) or provider configcapabilities.script wins when both are set.
compatibilityDateprovider config onlyNo ScriptCapConfig field. Sets the Worker compatibility date of the loaded isolate. Defaults to the runner's built-in date.
loaderconstructor 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.

Source

Released under the MIT License.