Script Module
The script capability lets your agent run lightweight JavaScript in a fast, ephemeral V8 isolate — Cloudflare's Worker Loader (Dynamic Workers). Each run loads ONE fresh isolate, executes the code, and Cloudflare tears it down. No container, no cold start, no durable state.
This is the lightweight half of a dual-tier story. Where the code capability gives you a full container interpreter (Python + JavaScript, persistent Jupyter-style contexts, a real filesystem), script gives you a near-instant JS-only isolate for quick one-off compute — roughly two orders of magnitude cheaper than spinning a container, at the cost of any persistence or non-JS language support. The two capabilities can coexist on a single workspace (see the Cloudflare Sandbox provider), so an agent can reach for the cheap isolate by default and escalate to the container only when it genuinely needs one.
Interface
script REUSES the same CodeInterpreter interface as the code capability — there is no separate "script" type. It surfaces on the workspace as Workspace.script (a sibling field to Workspace.code):
interface Workspace {
// ...
readonly code?: CodeInterpreter; // full container interpreter
readonly script?: CodeInterpreter; // lightweight isolate runner
// ...
}The isolate runner implements CodeInterpreter with the stateless shape:
class WorkerLoaderScriptRunner implements CodeInterpreter {
readonly languages: readonly string[] = ['javascript'];
readonly isStateful = false;
runCode(language: string, code: string, opts?: RunCodeOptions): Promise<RunCodeResult>;
// createContext / runInContext / deleteContext are ABSENT (stateless)
}Because the runner is isStateful: false, the optional context methods (createContext, runInContext, deleteContext) are NOT present — there is no kernel to keep variables alive between calls. Every runCode(lang, code) is fully independent: a fresh isolate, run, discard. See Stateless vs stateful on the code page for the underlying flag semantics; the script runner is always on the stateless side.
Capability config
interface ScriptCapConfig {
readonly languages?: readonly string[]; // default ['javascript']
readonly network?: 'off' | 'allow'; // default 'off' (egress blocked)
readonly maxDurationMs?: number; // optional per-run wall-clock cap
}languages— advertised language set for the auto-injected tool's description. Defaults to['javascript']. The runner only executes'javascript'(v1).network— outbound egress for executed code. Defaults to'off', which wires the isolate'sglobalOutboundtonull(allfetch/network blocked). Set to'allow'to permit egress.maxDurationMs— optional wall-clock cap per run. When set, the runner aborts the isolate viaAbortSignal.timeout(maxDurationMs). The tool'stimeoutMsargument overrides it per-call.
Where config goes. Declare it on the workspace's capabilities:
workspace: {
provider: { kind: 'cloudflare-dynamic-worker' /* or 'cloudflare-sandbox' */ },
capabilities: {
script: { network: 'allow', maxDurationMs: 5000 },
},
},The capability-level config is honored by BOTH the cloudflare-dynamic-worker and cloudflare-sandbox providers. On both, the capability config takes precedence over any provider-level default (e.g. the dynamic-worker provider's network / maxDurationMs kind-level options) — the per-capability declaration is the more specific one and wins. The merged values are persisted on the workspace ref so they round-trip through resolve() after a runtime boundary.
Auto-injected tools
For a workspace with script declared, the framework injects exactly ONE tool:
| Tool | Schema | Returns | When |
|---|---|---|---|
workspace_script | { code: string; language?: string; timeoutMs? } | RunCodeResult | Always (when script is declared) |
Unlike code, there are no context-management tools — the runner is stateless.
The tool returns a RunCodeResult ({ outputs, exitCode, error? }). The isolate captures console.log/info/debug as stdout outputs and console.warn/error as stderr; the return value of the script (if any) becomes a result output. As with run_code, the tool always wires an onOutput callback that emits workspace_code_output events to the agent's event stream.
JS-only. The language argument defaults to 'javascript'. Passing any other language does NOT throw — the runner returns a RunCodeResult with exitCode: 1 and an error message pointing the LLM at workspace_run_code for other languages:
WorkerLoaderScriptRunner: language 'python' is not supported (the script
runner is JavaScript-only; use workspace_run_code for other languages).script vs code — when to use which
The framework's system-prompt fragment tells the LLM how to choose between the two tiers when both are present:
Prefer
workspace_scriptfor quick one-off computations; useworkspace_run_codewhen you need a full environment, persistence, or other languages.
Reach for script when… | Reach for code when… |
|---|---|
| Quick one-off JS compute (parse, reshape) | You need a full environment (installed packages, a real fs) |
| You want the cheapest, fastest path | You need persistence across calls (Jupyter-style contexts) |
| The work is pure JavaScript | You need Python or any non-JS language |
When an agent declares both code and script, the LLM sees both tools and the guidance above, and routes accordingly.
Provider support matrix
| Provider | script supported |
|---|---|
| In-Memory | ❌ |
| Local Bash | ❌ |
| Local Sandbox | ❌ |
| Docker | ❌ |
| Cloudflare Filestore | ❌ |
| Cloudflare Dynamic Worker | ✅ (script-ONLY provider — exposes script and nothing else) |
| Cloudflare Sandbox | ✅ (opt-in — requires the provider's loader Worker-Loader binding; enables dual-tier alongside code) |
On cloudflare-sandbox, script is opt-in: it only materializes when the provider was constructed with a Worker Loader binding (the loader option). Declaring capabilities.script WITHOUT a loader binding fails fast at open() with a WorkspaceFailedError — a loader-less runner can never run. With the binding wired, script runs as a SEPARATE tier alongside the container code interpreter (dual-tier).
Security note
- Network off by default.
network: 'off'wires the isolate'sglobalOutboundtonull, blocking all egress. Setnetwork: 'allow'only when the script legitimately needs to reach the network. - The ephemeral V8 isolate IS the boundary. Each run loads a fresh, single-use isolate that Cloudflare tears down — the runner string-templates the user code into the isolate's main module, which is acceptable precisely because the isolate (not input sanitization) is the security boundary. A syntax error surfaces as a run failure; the code cannot escape the worker.
- No durable filesystem. Scripts have only ephemeral scratch state, discarded after the run. Nothing the script writes survives to the next call.
For the broader threat model and hardening guidance, see Workspaces Security. For provider-specific setup (Worker Loader bindings, wrangler config), see Cloudflare Dynamic Worker and Cloudflare Sandbox.
Source
- Runner:
packages/runtime-cloudflare/src/workspaces/script/runner.ts - Tool injection:
packages/core/src/workspace/tool-injection.ts(search formakeScriptTools) - Provider:
packages/runtime-cloudflare/src/workspaces/script/provider.ts