# How workspace.runtime.exec Handles Structured Input for Callable Backends in Cloudflare Computer

> Learn how workspace runtime exec handles structured input for callable backends in Cloudflare Computer using JSON encoding and Cap Proto RPC for type-safe execution.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: internals
- Published: 2026-08-07

---

**`workspace.runtime.exec` accepts a structured `input` object that is JSON-encoded and transmitted to callable backends via Cap'n Proto RPC, enabling type-safe, data-driven function execution within Cloudflare's Computer framework.**

The Cloudflare Computer repository provides a workspace-based execution environment where backends can operate in different modes. When a backend is marked as **callable**, it receives structured input rather than raw command strings. This article examines the complete data flow—from the `exec` method through backend resolution to RPC transport—using the actual source code implementation.

## What Are Callable Backends?

Callable backends are a specialized backend type in Computer that expose a function-like interface. Unlike traditional command-based backends that parse shell-style input, callable backends receive **deserialized JavaScript objects** directly.

A backend becomes callable by setting `callable: true` in its registration:

```ts
const callableBackend = createBackend({
  id: "my-fn",
  type: "callable",
  callable: true,  // Marks this backend as callable
  async connect({ db, fs }) {
    return {
      exec: async ({ source, input }) => {
        // `input` is the structured payload from the caller
        const result = await processData(input);
        return { id: generateId(), events: [], sync: "none" };
      },
      // getExec, killExec, disposeExec implementations...
    };
  },
});

```

The `callable` flag causes the backend ID to be included in `callableBackendIds`, which `WorkspaceRuntime` uses to route execution requests appropriately.

## The Execution Flow: From exec() to Backend

### Step 1: Workspace Creates the Runtime

In [[`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts)](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts), the `Workspace` class constructs a `WorkspaceRuntime` instance during initialization:

```ts
// Workspace constructor – lines 31-32
this.runtime = new WorkspaceRuntime({
  callableBackendIds: this.getCallableBackendIds(),
  backendHandle: (id) => this.resolveBackendHandle(id),
});

```

The `callableBackendIds` set determines which backends receive structured input handling versus traditional command execution.

### Step 2: Building the Execution Request

When you call `workspace.runtime.exec()`, the method constructs a `ModuleExecutionInput` object. From [[`packages/computer/src/runtime/runtime.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/runtime/runtime.ts)](https://github.com/cloudflare/computer/blob/main/packages/computer/src/runtime/runtime.ts) (lines 64-71):

```ts
// WorkspaceRuntime.exec – input handling
async exec(
  source: string,
  options: ExecOptions = {}
): Promise<ExecutionHandle> {
  const input: ModuleExecutionInput = {
    source,
    input: options.input,        // Structured input object (JSON-serializable)
    // ... additional metadata
  };
  
  // Backend resolution and RPC transmission
  const backendId = options.backend ?? this.defaultBackendId;
  const handle = await this.backendHandle(backendId);
  // ...
}

```

The `options.input` field carries the structured payload. This is the critical path where JavaScript objects become backend-bound data.

### Step 3: Backend Resolution and Callable Routing

The runtime checks whether the target backend is callable (lines 82-89 in [`runtime.ts`](https://github.com/cloudflare/computer/blob/main/runtime.ts)):

```ts
// Backend resolution with callable check
const isCallable = this.callableBackendIds.has(backendId);
if (isCallable) {
  // Route to module-backend callable interface
  return this.executeCallable(backendId, input);
}
// Otherwise, fall back to standard command execution

```

Callable backends bypass shell-style parsing entirely. The `source` parameter becomes largely ceremonial—only `input` matters for execution semantics.

### Step 4: RPC Transport via Cap'n-Web

The structured input traverses the wire through [[`packages/computer/src/runtime/wire.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/runtime/wire.ts)](https://github.com/cloudflare/computer/blob/main/packages/computer/src/runtime/wire.ts) (lines 30-45):

```ts
// RPC envelope creation
function createExecutionEnvelope(input: ModuleExecutionInput): ArrayBuffer {
  const serialized = JSON.stringify(input.input);  // JSON-encoding
  // Build Cap'n Proto message with serialized payload
  return capnp.serialize({
    source: input.source,
    payload: serialized,           // Structured data as JSON string
    executionId: input.id,
  });
}

```

**Key constraint**: The `input` object must be **JSON-serializable**. Functions, circular references, and `undefined` values are stripped or cause serialization failures.

### Step 5: Backend Execution and Result Return

The callable backend receives the deserialized input and returns a `ModuleExecutionEnvelope`. The result path (lines 126-133 in [`runtime.ts`](https://github.com/cloudflare/computer/blob/main/runtime.ts)):

```ts
// Result handling
async result(): Promise<any> {
  const envelope = await this.awaitResponse();
  // Decode JSON payload from backend
  return JSON.parse(envelope.result);
}

```

The round-trip preserves structured data: caller's object → JSON → backend → result object → JSON → caller.

## Practical Code Examples

### Basic Structured Input Execution

```ts
// Execute with explicit structured input
const handle = await ws.runtime.exec("compute", {
  backend: "analytics-processor",
  input: {
    dataset: "user-events",
    timeRange: { start: "2024-01-01", end: "2024-01-31" },
    aggregations: ["count", "sum"],
    filters: { region: "US" }
  },
});

const result = await handle.result();
console.log(result.totals);  // Structured result from backend

```

### Tagged Template Syntax

Computer supports convenient tagged templates that automatically structure input:

```ts
// Template literal form – first argument becomes command, 
// interpolated values become input
await ws.runtime.exec`analytics-processor ${{
  dataset: "user-events",
  timeRange: { start: "2024-01-01", end: "2024-01-31" },
}}`;

```

The template engine extracts the command identifier and packages the interpolated object as `input`.

### Streaming Results from Callable Backends

```ts
const handle = await ws.runtime.exec("stream-process", {
  backend: "event-stream",
  input: { source: "kafka-orders", batchSize: 100 },
});

// Consume incremental results
for await (const chunk of handle.events()) {
  console.log("Batch processed:", chunk.count);
}

// Final aggregation
const final = await handle.result();

```

### Error Handling with Structured Context

```ts
try {
  await ws.runtime.exec("validate", {
    backend: "schema-validator",
    input: { document: malformedData },
  });
} catch (e) {
  // Error includes structured validation failures if backend provides them
  console.error("Validation failed:", e.details?.failures);
}

```

## Input Validation and Constraints

### JSON Serialization Requirements

| Valid Input | Invalid Input |
|-------------|---------------|
| Plain objects `{a: 1}` | Functions `() => {}` |
| Arrays `[1, 2, 3]` | Circular references |
| Primitives `string`, `number`, `boolean`, `null` | `undefined` (omitted) |
| Nested structures | `Symbol`, `BigInt` |

The runtime performs no schema validation—backends must defend against malformed input.

### Execution ID Constraints

From [`runtime.ts`](https://github.com/cloudflare/computer/blob/main/runtime.ts) validation logic:

```ts
// Execution ID validation
if (!input.id || input.id.length === 0) {
  throw new Error("Execution ID required");
}
if (input.id.length > 256) {
  throw new Error("Execution ID exceeds 256 byte limit");
}

```

## Connection Resilience

If the underlying RPC stream fails, `WorkspaceRuntime` invalidates the cached backend handle (lines 145-152 in [`runtime.ts`](https://github.com/cloudflare/computer/blob/main/runtime.ts)):

```ts
// Transport error recovery
} catch (e) {
  if (isTransportError(e)) {
    this.backendHandleCache.delete(backendId);  // Force reconnect
  }
  throw e;
}

```

The next `exec` call triggers fresh connection establishment without caller intervention.

## Summary

- **`workspace.runtime.exec`** accepts structured `input` through the `options.input` field, enabling data-driven backend execution
- **Callable backends** receive JSON-deserialized objects directly, bypassing shell-style command parsing
- **Transport layer** uses Cap'n-Web RPC with JSON-encoded payloads; all data must survive `JSON.stringify`
- **Backend resolution** checks `callableBackendIds` to determine execution path
- **Result handling** automatically decodes JSON responses into JavaScript values
- **Connection failures** trigger transparent reconnection via cache invalidation

## Frequently Asked Questions

### What happens if I pass non-JSON data as input?

The runtime attempts `JSON.stringify()` during RPC envelope creation. Non-serializable values like functions or circular references typically cause `TypeError` exceptions. The specific behavior depends on your JSON serializer configuration—some values may be silently omitted rather than throwing.

### Can I use callable backends without specifying a command string?

Yes. For callable backends, the `source` parameter in `exec(source, options)` is largely ignored by the backend implementation. You can pass any string (commonly the backend ID for clarity) or use the tagged template form where the template literal's first part becomes the nominal command.

### How does the runtime distinguish callable from non-callable backends?

During `Workspace` construction, backends with `callable: true` have their IDs collected into a `Set` passed to `WorkspaceRuntime` as `callableBackendIds`. The runtime performs an `O(1)` lookup via `this.callableBackendIds.has(backendId)` to select the execution path—callable backends skip shell parsing and receive direct object input.

### Is there a size limit on structured input?

There's no explicit byte limit enforced by [`runtime.ts`](https://github.com/cloudflare/computer/blob/main/runtime.ts), but practical constraints apply: (1) JSON serialization overhead, (2) Durable Object message size limits (typically 1MB for WebSocket messages), and (3) execution ID length limit of 256 bytes. Large inputs should use streaming patterns or external storage references.