How to Extend and Customize Runtime Types in Cloudflare Computer
Yes—you can extend and customize runtime types in Cloudflare Computer by implementing the WorkspaceModuleBackend interface and registering your backend with the Workspace constructor.
Cloudflare Computer exposes a pluggable runtime architecture that lets you swap or augment execution backends without modifying core code. The workspace.runtime surface acts as a thin router that delegates calls to registered backends, making it possible to add custom execution environments, new protocols, or extended type systems.
Understanding the Runtime Extension Architecture
The runtime layer in Cloudflare Computer separates routing logic from execution logic. In packages/computer/src/workspace.ts, the Workspace class accepts a backends array that defines which execution engines are available. The router resolves backend IDs at runtime and forwards runtime.exec, runtime.getExec, and related calls to the appropriate implementation.
This design means you can:
- Add entirely new execution backends (custom containers, WASM modules, remote sandboxes)
- Replace built-in backends with specialized implementations
- Extend the type system for return values and event payloads
The Backend Registration Mechanism
When constructing a Workspace, you provide custom backends via the backends option:
import { Workspace } from "@cloudflare/computer";
const ws = new Workspace({
storage: makeStorage(),
backends: [myCustomBackend], // Register your implementation
defaultBackendId: "my-backend", // Optional: make it the default
});
The backends parameter accepts WorkspaceRegisteredBackend objects as defined in packages/computer/src/workspace.ts. The router uses these registrations to:
- Match the
backendoption passed toruntime.exec()calls - Fall back to
defaultBackendIdor the first registered backend when unspecified
Implementing the WorkspaceModuleBackend Interface
Every custom backend must conform to the WorkspaceModuleBackend contract in packages/computer/src/runtime/types.ts. The required shape includes:
| Property | Type | Description |
|---|---|---|
protocol |
"module" |
Fixed discriminator for module-type backends |
id |
string |
Unique identifier used for routing |
type |
string |
Human-readable type name |
connect(host) |
Promise<WorkspaceModuleBackendHandle> |
Factory that returns an execution handle |
The connect method receives a host context and must return a WorkspaceModuleBackendHandle implementing the execution primitives.
Building a Custom Backend Handle
The handle returned by connect must implement four core methods plus an optional close method, as specified in packages/computer/src/runtime/types.ts:
exec(input)— Start a new execution, return aModuleExecutionEnvelopewith an event streamgetExec(id)— Reconnect to an existing execution (optional for stateless backends)killExec(id)— Terminate a running executiondisposeExec(id)— Clean up resources for a completed executionclose()— Release global backend resources (optional)
The event stream must follow the WorkspaceRuntimeExecHandle semantics: ordered events with id, seq, name, and value fields, terminating with an exit event.
Complete Custom Backend Example
// echo-backend.ts
import type {
WorkspaceModuleBackend,
WorkspaceModuleBackendHandle,
ModuleExecutionInput,
ModuleExecutionEnvelope,
} from "@cloudflare/computer";
export const echoBackend: WorkspaceModuleBackend = {
protocol: "module",
id: "echo",
type: "demo-echo",
async connect(host) {
const handle: WorkspaceModuleBackendHandle = {
async exec(input: ModuleExecutionInput): Promise<ModuleExecutionEnvelope> {
const resultId = input.id ?? crypto.randomUUID();
return {
id: resultId,
runtimeId: undefined,
events: new ReadableStream({
start(controller) {
// Echo the input as stdout
const payload = typeof input.source === "string" ? input.source : "";
controller.enqueue({
id: resultId,
seq: 0,
name: "stdout",
value: payload,
});
controller.enqueue({
id: resultId,
seq: 1,
name: "exit",
code: 0,
});
controller.close();
},
}),
};
},
async getExec() {
throw new Error("Echo backend does not support getExec");
},
async killExec() {
// No-op: executions complete immediately
},
async disposeExec() {
// No-op: nothing to dispose
},
async close() {
// Clean up any global resources
},
};
return handle;
},
};
Registering and Using Your Custom Runtime
With the backend defined, wire it into your workspace and invoke it through the standard runtime.exec API:
import { Workspace } from "@cloudflare/computer";
import { echoBackend } from "./echo-backend";
const ws = new Workspace({
storage: makeStorage(),
backends: [echoBackend],
});
// Execute using the custom backend
const handle = await ws.runtime.exec("Hello, Cloudflare Computer!", {
backend: "echo", // Select by backend ID
encoding: "utf8",
});
// Consume the event stream
for await (const event of handle) {
if (event.name === "stdout") {
console.log("Received:", event.value);
}
}
const result = await handle.result();
console.log("Exit code:", result.exitCode); // 0
The backend option in runtime.exec selects your registered implementation. Omit it to use the defaultBackendId or first available backend.
Extending Runtime Value Types
To customize the type system for values flowing through your backend, extend the WorkspaceRuntimeValue union in packages/computer/src/runtime/types.ts and re-export from your package:
// custom-types.ts
import type { WorkspaceRuntimeValue } from "@cloudflare/computer";
export type ExtendedRuntimeValue =
| WorkspaceRuntimeValue
| { __tag: "custom-struct"; payload: Record<string, unknown> };
Consumers importing your extended types gain type safety for backend-specific return values while maintaining compatibility with the base runtime API.
Key Implementation Files
| File | Purpose |
|---|---|
packages/computer/src/runtime/types.ts |
Defines WorkspaceModuleBackend, WorkspaceModuleBackendHandle, and WorkspaceRuntimeValue—the core contracts for customization |
packages/computer/src/workspace.ts |
Implements backend registration and routing logic in the Workspace class |
packages/computer/src/runtime/wire.ts |
Low-level event stream protocol used by all backends |
docs/05_runtime_interface.md |
Public API documentation for workspace.runtime |
docs/16_code_execution.md |
Usage patterns and backend selection semantics |
Summary
- Backend registration happens at
Workspaceconstruction via thebackendsarray inpackages/computer/src/workspace.ts - The
WorkspaceModuleBackendinterface inpackages/computer/src/runtime/types.tsdefines the contract for custom runtimes - Four core methods (
exec,getExec,killExec,disposeExec) plus optionalclosemust be implemented - Event streams must conform to
WorkspaceRuntimeExecHandlesemantics with ordered, named events terminating inexit - Type extensions are achieved by augmenting
WorkspaceRuntimeValueand re-exporting from your package - Runtime selection uses the
backendoption inruntime.execcalls, routed through the internal backend registry
Frequently Asked Questions
How do I make my custom backend the default runtime?
Pass defaultBackendId matching your backend's id in the Workspace constructor. When runtime.exec is called without a backend option, the router selects this default.
Can I override the built-in container or bash backends?
Yes—register a backend with the same id as a built-in (e.g., "container", "bash") and it will take precedence. Alternatively, omit built-in backends from the backends array entirely and supply only your implementations.
What happens if my backend doesn't support getExec?
Throw an error from getExec as shown in the echo backend example. This is valid for stateless or ephemeral execution models where reconnection isn't meaningful. The Workspace runtime will propagate this error to callers attempting to reconnect.
How do I debug event stream issues in my custom backend?
Refer to packages/computer/src/runtime/wire.ts for the exact wire protocol. Ensure your stream emits events with monotonic seq numbers, includes all required fields (id, name, value), and always terminates with an exit event—otherwise consumers will hang awaiting completion.
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 →