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 the Workspace constructor.
  • callableBackendIds – Tracks backends that accept structured input. Stored in WorkspaceRuntime.isCallable and WorkspaceRuntime.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" converts stdout/stderr from Uint8Array to strings
  • File system access uses the WorkspaceRuntimeFilesystem API (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 Computer
  • worker-javascript backend runs ES modules with optional structured input and returns values via result.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 input on non-callable backends with a clear error message
  • Reference packages/computer/src/runtime/runtime.ts for the router implementation and packages/computer/src/runtime/types.ts for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →