Structured Value Returns vs. Stub‑Bearing Returns in Cloudflare Computer

Stub‑bearing returns contain live RPC capability stubs that can be invoked remotely, while plain structured value returns contain only JSON‑serializable data with no remote invocation ability.

The Cloudflare Computer repository distinguishes between two kinds of execution results returned by Workspace.runtime.exec. Understanding this difference is essential for writing modules that exchange data or capabilities across the RPC boundary between a caller and the executing Durable Object.

What Workspace.runtime.exec Returns

Every call to Workspace.runtime.exec resolves to a WorkspaceRuntimeResult object defined in packages/computer/src/runtime/types.ts. This result object contains an optional value field that may carry execution output back to the caller.

Whether that value contains plain structured data or stub‑bearing data depends entirely on what the executed module returns and which backend runs it.

Plain Structured Values (No Stubs)

A plain structured value is returned when a module's default export produces a JSON‑compatible object containing only primitives, arrays, and plain objects—no capability stubs.

According to the execution contract in docs/16_code_execution.md at lines 45‑61, only the worker‑javascript backend may populate the value field. When your module returns pure data, the caller receives ordinary JavaScript objects that cannot trigger further RPC calls.

Characteristics of Plain Returns

  • Safe for any consumer — no lifecycle management required
  • Fully serializable — can be cached, logged, or passed freely
  • No remote references — disconnection of the caller does not affect the data
/* my-module.js */
export default async function (opts) {
  // Returns a plain JSON object
  return { message: "Hello", count: 3 };
}

/* caller */
const handle = await workspace.runtime.exec("my-module.js", {
  backend: "worker-javascript",
});
const { value } = await handle.result();   // → { message: "Hello", count: 3 }

Command backends like container-shell and worker-shell never set value; they always return undefined. Only the worker‑javascript module backend can emit structured results of any kind.

Stub‑Bearing Returns

A stub‑bearing return occurs when the returned structure contains capability stubs (objects extending RpcTarget) that cross the RPC boundary. These are concrete representations of remote services—such as a WorkspaceStub obtained via env.HOST.getWorkspace().

As documented in docs/08_capnweb_interface.md at lines 31‑49, stubs are created on the wire and deserialized into live proxies on the caller side. The caller can then invoke methods on these proxies, which execute remotely in the Durable Object.

Characteristics of Stub‑Bearing Returns

  • Live remote references — method calls cross the RPC boundary
  • Lifecycle dependency — stubs are tied to their parent stub's lifetime
  • Disposal requirement — must respect the stub‑disposal contract in docs/11_lifecycle.md at lines 239‑242
/* my-module.js */
export default async function (opts) {
  // Return a live capability stub (the Workspace itself)
  const ws = await env.HOST.getWorkspace(); // ← stub created via RPC
  return { workspace: ws, note: "you can call ws.fs.* now" };
}

/* caller */
const handle = await workspace.runtime.exec("my-module.js", {
  backend: "worker-javascript",
});
const { value } = await handle.result();   // → { workspace: WorkspaceStub, … }

// `value.workspace` is a stub; you can now do:
await value.workspace.fs.readFile("/README.md");

Managing Stub Lifetimes

Stub‑bearing returns introduce resource management obligations. The stub‑disposal contract documented in docs/11_lifecycle.md specifies that child stubs are automatically disposed when their parent stub is garbage‑collected or explicitly released.

When WorkspaceStub is implemented in packages/computer/src/stub.ts, it tracks child stubs created during RPC calls. Failing to dispose of the parent stub can leak remote object references.

import { stubSnapshot, enableStubTracking } from "@cloudflare/computer-rpc/debug";

enableStubTracking();                     // turn on leak tracking
const exec = await workspace.runtime.exec("my-module.js", { backend: "worker-javascript" });
const { value } = await exec.result();

await exec.disposeExec({ id: exec.id });   // releases the exec log
// The stub inside `value` is automatically disposed when the parent stub (`workspace`) is GC‑ed
console.log(stubSnapshot());               // shows no leaked stubs

Key Implementation Files

File Purpose
packages/computer/src/runtime/types.ts Defines WorkspaceRuntimeResult and the optional value field
packages/computer/src/stub.ts Implements WorkspaceStub and RPC stub plumbing
docs/16_code_execution.md Documents execution result shapes and backend constraints
docs/08_capnweb_interface.md Describes WorkspaceRPC root stub and wire transmission
docs/11_lifecycle.md Explains parent‑child stub lifetimes and disposal

Summary

  • Plain structured returns provide JSON‑only data with no RPC capabilities—safe, cacheable, and lifecycle‑free
  • Stub‑bearing returns embed live RpcTarget stubs that enable remote method calls but require proper disposal
  • Only the worker‑javascript backend can return either kind of value; command backends never populate this field
  • Stub lifetimes follow the parent‑child disposal contract—child stubs die with their parent

Frequently Asked Questions

Can I return stubs from a container‑shell or worker‑shell backend?

No. According to the execution contract in docs/16_code_execution.md, command backends (container-shell, worker-shell) never set the value field—they always return undefined. Only the worker‑javascript module backend can emit structured results, whether plain or stub‑bearing.

How do I detect if a returned value contains stubs?

Check for the presence of RpcTarget properties or use the runtime's stub tracking utilities. In practice, you control this by design: if your module imports and returns objects from env.HOST or other RPC sources, the result contains stubs. Pure JSON objects contain none.

What happens if I don't dispose of a stub‑bearing result?

The parent stub and its children remain referenced until garbage collection runs. Under the stub‑disposal contract documented in docs/11_lifecycle.md at lines 239‑242, this can delay cleanup of remote resources. Use stubSnapshot() from @cloudflare/computer-rpc/debug to detect leaks in development.

Can I mix plain data and stubs in the same return value?

Yes. A worker‑javascript module may return an object with both primitive properties and stub properties. The caller receives a hybrid structure where some fields are plain data and others are live RPC proxies.

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 →