How to Integrate Custom Code with Cloudflare Computer Runtime Types
Use workspace.runtime.exec() to run arbitrary code as either shell commands or ES modules, with structured input support for callable backends like worker-javascript.
The Cloudflare Computer SDK provides a flexible execution environment for running custom code inside Durable Objects. Whether you need to execute shell commands in containers or run JavaScript modules with structured data, the WorkspaceRuntime class in packages/computer/src/runtime/runtime.ts handles routing, validation, and result streaming.
Understanding the Runtime Router Architecture
The WorkspaceRuntime class acts as a central router between your application code and configured backends. All execution flows through workspace.runtime.exec(source, options), which delegates to the appropriate backend based on the backend option.
Backend Selection and Validation
Backends are identified by opaque strings you assign during construction. The router validates two critical properties before execution:
backend– Specifies which backend interprets the source string. Defaults to the first backend in theWorkspaceconstructor.callableBackendIds– Tracks backends that accept structuredinput. Stored inWorkspaceRuntime.isCallableandWorkspaceRuntime.callableBackendIds(runtime.ts#L22-L28).
If you provide options.input to a non-callable backend, the router throws the canonical error message produced by notCallableMessage (runtime.ts#L26-L28).
Execution Flow
// Core execution pipeline (simplified)
const backendHandle = await this.getBackend(backend); // runtime.ts#L55-L82
const envelope = await runtime.exec(moduleExecutionInput);
const handle = wrapModuleHandle(envelope, syncBracket); // runtime.ts#L86-L124
The returned WorkspaceRuntimeExecHandle extends ReadableStream<WorkspaceRuntimeEvent> and provides a result() method for eager consumption.
Running Custom JavaScript Modules
The worker-javascript backend executes source strings as ECMAScript modules. Your module must export a default async function; its return value becomes result.value.
Basic Module Execution
import { Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
const ws = new Workspace({
storage, // DurableObjectStorage-like instance
backends: [
new WorkerJavaScriptBackend({ id: "worker-javascript" })
],
useThink: false,
});
const handle = await ws.runtime.exec(
`
import fs from "node:fs/promises";
export default async () => {
const data = await fs.readFile("/workspace/package.json", "utf8");
return JSON.parse(data).name;
};
`,
{ backend: "worker-javascript", encoding: "utf8" }
);
const result = await handle.result();
console.log(result.value); // "my-project"
Key implementation details from the source:
- The SDK wraps your source in a temporary file before loading (worker-javascript.ts)
encoding: "utf8"convertsstdout/stderrfromUint8Arrayto strings- File system access uses the
WorkspaceRuntimeFilesystemAPI (types.ts#L35-L66)
Passing Structured Input to Callable Backends
Only backends with callable: true accept the input option. The value is serialized as a WorkspaceRuntimeValue (JSON-compatible: null, boolean, number, string, array, object) and passed as the first argument to your exported function.
const handle = await ws.runtime.exec(
`
export default async (input) => {
return { greeting: \`Hello, \${input.name}!\` };
};
`,
{
backend: "worker-javascript",
input: { name: "Alice" }, // structured argument
encoding: "utf8"
}
);
const { value } = await handle.result();
console.log(value.greeting); // "Hello, Alice!"
Attempting to use input with a non-callable backend triggers the validation error in WorkspaceRuntime.exec (runtime.ts#L55-L82).
Consuming Results: Streaming vs. Eager
The WorkspaceRuntimeExecHandle provides two mutually exclusive consumption patterns, enforced in wrapModuleHandle (runtime.ts#L86-L124).
Streaming Events for Real-Time Output
const handle = await ws.runtime.exec("npm test", {
backend: "container-shell",
encoding: "utf8"
});
for await (const ev of handle) {
if (ev.name === "stdout") process.stdout.write(ev.value);
if (ev.name === "stderr") process.stderr.write(ev.value);
if (ev.name === "exit") console.log(`Exit code: ${ev.value}`);
}
Eager Result Consumption
const result = await handle.result(); // internally consumes the stream
Critical constraint: Calling result() after iterating the stream—or iterating after calling result()—throws runtime handle already consumed.
Sync Brackets for Container Backends
Container backends like container-shell automatically synchronize filesystem state:
| Phase | Operation | Purpose |
|---|---|---|
| Pre-execution | Push | Sync local VFS changes to container store |
| Execution | Spawn | Run command inside computerd |
| Post-execution | Pull | Sync container store back to local VFS |
The WorkspaceRuntimeResult reports synchronization metrics via drainModuleResult (runtime.ts#L105-L148):
const result = await handle.result();
console.log(result.sync); // { status: "complete" | "pending" }
console.log(result.pushed); // files pushed to container
console.log(result.pulled); // files pulled from container
console.log(result.skipped); // unchanged files
Complete Integration Examples
Shell Command with Default Backend
const handle = await ws.runtime.exec("ls -la /workspace", {
encoding: "utf8"
});
const { stdout } = await handle.result();
Module with Computation and Structured Return
const handle = await ws.runtime.exec(
`
export default async (payload) => {
return { sum: payload.a + payload.b };
};
`,
{
backend: "worker-javascript",
input: { a: 3, b: 7 }
}
);
const { value } = await handle.result();
console.log(value.sum); // 10
Filesystem Operations in Module Context
const handle = await ws.runtime.exec(
`
import fs from "node:fs/promises";
export default async () => {
const files = await fs.readdir("/workspace");
const stats = await Promise.all(
files.map(f => fs.stat("/workspace/" + f).then(s => ({ name: f, size: s.size })))
);
return stats;
};
`,
{ backend: "worker-javascript" }
);
Backend Configuration and Security
Backend IDs are arbitrary strings defined at construction time. The router performs no authorization—your gateway must validate backend selection against a server-side allowlist.
Example configuration from the reference implementation (examples/think/src/agent.ts#L92-L103):
const ws = new Workspace({
storage,
backends: [
new WorkerShellBackend({ id: "worker-shell" }),
new WorkerJavaScriptBackend({ id: "worker-javascript" }),
new ContainerBackend({ id: "container-shell" })
]
});
Summary
workspace.runtime.exec()is the single entry point for all custom code execution in Cloudflare Computerworker-javascriptbackend runs ES modules with optional structuredinputand returns values viaresult.value- Streaming and eager consumption are mutually exclusive—choose based on whether you need real-time progress
- Container backends automatically handle filesystem synchronization via push/pull brackets
- Backend validation prevents
inputon non-callable backends with a clear error message - Reference
packages/computer/src/runtime/runtime.tsfor the router implementation andpackages/computer/src/runtime/types.tsfor complete type definitions
Frequently Asked Questions
What backends support structured input in Cloudflare Computer?
Only backends with callable: true accept structured input. Currently this includes worker-javascript. The WorkerJavaScriptBackend class sets this property during construction, and the WorkspaceRuntime router validates against callableBackendIds before execution. Shell and container backends execute commands without structured argument passing.
Can I use both streaming and result() on the same execution handle?
No. The WorkspaceRuntimeExecHandle enforces single-consumer semantics in wrapModuleHandle (runtime.ts#L86-L124). Choose streaming when you need incremental output (UI progress, logs) or result() when you only need the final return value. Attempting both throws runtime handle already consumed.
How does filesystem persistence work with container backends?
Container backends automatically wrap execution in a sync bracket: pending local changes push before the command starts, and container state pulls back after completion. The WorkspaceRuntimeResult includes pushed, pulled, skipped counts and a sync status object. This ensures durability without manual intervention, implemented in drainModuleResult (runtime.ts#L105-L148).
What module formats does the JavaScript backend support?
The worker-javascript backend requires ES modules with a default async function export. The source string is wrapped in a temporary file and loaded as a module. You can use import statements including the built-in node:fs/promises shim for VFS access. CommonJS (require, module.exports) is not supported in this backend.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →