How to Configure Multiple Backends with Stable Selector IDs and Route Exec Calls in Cloudflare Computer

Use explicit id values when creating backends, register them in a map passed to the Workspace constructor, then reference those IDs in the backend field of ExecOptions to route exec calls to specific runtimes.

Cloudflare Computer lets a single Workspace manage multiple execution environments—containers, Worker shells, or JavaScript isolates—simultaneously. Each backend is identified by a stable selector ID that you control, enabling deterministic routing of commands via Workspace.runtime.exec. This guide walks through the configuration pattern implemented in cloudflare/computer, referencing actual source files from the repository.

Understanding Backend Selectors and Routing

The routing mechanism centers on the WorkspaceBackend interface defined in [backend.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backend.ts#L38-L48). Every backend exposes an id property that serves as its unique selector within a workspace.

// From backend.ts - the core interface
interface WorkspaceBackend {
  id: string;
  connect(host: string): Promise<BackendHandle>;
  // ... other properties
}

When you call ws.runtime.exec(), the workspace looks up the backend by the backend selector, lazily establishes a connection via connect(host), caches the resulting BackendHandle, and forwards the command to the appropriate RPC stub. This lookup happens in the Workspace class implementation in [workspace.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts).

Step 1: Create Backends with Explicit IDs

Always pass an id option to backend constructors. If omitted, a default ID is generated—but explicit IDs guarantee stability across restarts and prevent accidental mismatches when backend configurations change.

import { ContainerBackend, WorkerShellBackend } from "@cloudflare/computer";

// Explicit, stable selector IDs
const container = new ContainerBackend({ id: "container" });
const shell = new WorkerShellBackend({ id: "worker-shell" });

The ContainerBackend and WorkerShellBackend implementations set default IDs (container-shell and worker-shell respectively) when none is provided, as seen in their source definitions.

Step 2: Register Backends in the Workspace Constructor

Pass a map of id → backend to the Workspace constructor. The workspace owns this map and rejects duplicate IDs to ensure each selector resolves unambiguously.

const ws = new Workspace({
  backends: {
    container,           // key matches backend.id
    "worker-shell": shell,  // key matches backend.id
  },
});

This registration pattern is enforced by the Workspace constructor in [workspace.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts). The map keys must match the id properties of the backend instances.

Step 3: Route Exec Calls Using the Backend Selector

The ExecOptions type in [shell.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/shell.ts) includes an optional backend field. When provided, the workspace routes the command to the matching backend.

// Route to container backend
await ws.runtime.exec("ls -la /", { backend: "container" });

// Route to Worker shell backend
await ws.runtime.exec("echo $HOME", { backend: "worker-shell" });

The runtime.exec API exposed in [client.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/client.ts) handles this routing transparently—your code simply specifies the target backend by its stable ID.

Step 4: Configure Callable Backends (Optional)

Set callable: true in backend options to enable structured input/result pairs beyond raw shell commands. This flag is respected by the backend implementation and adds a higher-level RPC interface.

// Callable backends process JavaScript modules
await ws.runtime.exec(
  "module.exports = (input) => ({ ok: true, value: input * 2 });",
  { backend: "worker-shell" }
);

The same backend selector routing applies—only the execution semantics differ based on the backend's callable configuration.

Step 5: Manage Backend Lifecycle

Backend handles are cached per workspace lifetime. Call backendHandle.close() to release resources, or rely on Workspace.close() to clean up all backends automatically.

// Clean up all backends
await ws.close();

This ensures proper teardown of container processes, RPC stubs, and other runtime resources.

Complete Working Example

import {
  Workspace,
  ContainerBackend,
  WorkerShellBackend,
} from "@cloudflare/computer";

// 1. Create backends with stable selector IDs
const container = new ContainerBackend({ id: "container" });
const shell = new WorkerShellBackend({ id: "worker-shell" });

// 2. Register in workspace constructor
const ws = new Workspace({
  backends: {
    container,
    "worker-shell": shell,
  },
});

// 3. Route exec calls to specific backends
await ws.runtime.exec("apt-get update", { backend: "container" });
await ws.runtime.exec("curl https://api.example.com", { backend: "worker-shell" });

// 4. Clean up
await ws.close();

Key Source Files Reference

File Purpose
[backend.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backend.ts) WorkspaceBackend interface with id selector
[workspace.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts) Workspace class implementing backend resolution and routing
[shell.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/shell.ts) ExecOptions type with backend field
[client.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/client.ts) High-level runtime.exec API

Summary

  • Explicit IDs: Pass id to backend constructors for stable selectors across deployments
  • Registration map: Provide backends: { [id]: backend } to Workspace constructor; duplicates are rejected
  • Route via backend field: Use ExecOptions.backend in runtime.exec() calls to target specific runtimes
  • Lifecycle management: Close workspace or handles to release resources properly

Frequently Asked Questions

What happens if I omit the backend option in an exec call?

If no backend selector is provided, the workspace uses a default backend—typically the first registered or a system-defined fallback. Explicit selection is recommended for predictable behavior when multiple backends are configured.

Can I change a backend's ID after creating the workspace?

No. Backend IDs are fixed at construction time and validated during Workspace initialization. To use different IDs, create new backend instances and construct a fresh workspace.

How does the workspace handle backend connection failures?

Connections are established lazily via connect(host) and cached as BackendHandle instances. If a connection fails, the error propagates through the exec call. The workspace does not automatically retry or failover to other backends—your application code must handle those semantics.

Is the backend selector validated at compile time or runtime?

The selector is a string validated at runtime against registered backend IDs. TypeScript users can define union types for stricter checking, but the Workspace constructor performs runtime validation to reject duplicate or missing backend references.

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 →