Skip to content

Bindings

Bindings are a way to grant Dynamic Workers access to specific APIs and resources. They are similar to regular Workers bindings. However, Dynamic Worker bindings don't typically point at regular Workers platform resources like KV namespaces or R2 buckets. Instead, they point to anything you want.

When using Dynamic Workers, you can invent your own bindings to give to the Worker, by defining arbitrary Workers RPC interfaces.

Capability-based Sandboxing

Workers RPC — also known as Cap'n Web — is an RPC system designed to make it easy to present rich TypeScript interfaces across a security boundary. Cap'n Web implements a capability-based security model. That means, it supports passing objects "by reference" across RPC boundaries. When you receive an object reference (also known as a "stub") over RPC, you are implicitly granted the ability to call that object's methods; doing so makes further RPC calls back to the original object. Objects do not have any URL or global identifier, so the only way to address one is to have received a stub pointing to it — if you haven't received a stub, you can't call the object.

Capability-based Security is essential to the design of most, if not all, successful sandboxes, though it is often an implementation detail that users and even developers don't see. Android has Binder, Chrome has Mojo, and Cloudflare Workers has Cap'n Proto and Cap'n Web.

Dynamic Workers directly expose this power to you, the developer, so that you can build your own strong sandbox.

Custom Bindings with Dynamic Workers

Imagine you are using Dynamic Workers to implement an agent that can post messages to a chat room. Different agents will be able to post to different chat rooms, but any particular agent is only allowed to post to one specific chat room. The agent writes code, which you run in a Dynamic Worker.

You want to make sure the code can only access the specific chat room that the given agent is authorized for. One way to do this would be to pass the chat room name into the Dynamic Worker (or to the agent), and then verify that all requests coming out of the worker are addressed to the correct room, blocking them if not. However, Dynamic Workers offers a better approach: give the Worker a binding that represents the specific chat room.

To define a custom binding, your parent worker needs to implement a WorkerEntrypoint class and export it. In this case, we will be defining a class called ChatRoom. Of course, we don't want to export a new class for every possible chat room. Instead, we can specialize the interface for a specific room using ctx.props.

TypeScript
import { WorkerEntrypoint } from "cloudflare:workers";
// Define the ChatRoom RPC interface.
//
// This MUST be exported from the top-level module of the
// parent worker.
export class ChatRoom extends WorkerEntrypoint<Cloudflare.Env, ChatRoomProps> {
// Any methods defined on this class will be callable
// by the Dynamic Worker.
// Method to post a message to chat.
async post(text: string): Promise<void> {
let { apiKey, botName, roomName } = this.ctx.props;
// Prefix the message with the bot's name.
text = `[${botName}]: ${text}`;
// Send it to the chat service.
await postToChat(apiKey, roomName, text);
}
}
// Define a props type which specializes our `ChatRoom` for
// a particular client. This can be any serializable object
// type.
type ChatRoomProps = {
// API key to the remote chat API.
apiKey: string;
// Name of the room to post to.
roomName: string;
// Name of the bot posting.
botName: string;
};

Now we can load a Dynamic Worker and give it a Chat Room. To create the chat room RPC stub, we use ctx.exports, then we simply pass it into the Dynamic Worker Loader in the env object:

TypeScript
// Let's say our agent wrote this code.
let codeFromAgent = `
export class Agent extends WorkerEntrypoint {
async run() {
await this.env.CHAT_ROOM.post("Hello!");
}
}
`;
// Set up the props for our agent.
let props: ChatRoomProps = {
apiKey,
roomName: "#bot-chat",
botName: "Robo",
};
// Create a service stub representing our chat room
// capability. The system automatically creates
// `ctx.exports.ChatRoom` because our top-level module
// exported a `WorkerEntrypoint` called `ChatRoom`.
let chatRoom = ctx.exports.ChatRoom({ props });
// `chatRoom` is now an RPC service stub. We could
// call methods on it, like `chatRoom.post()`.
let worker = env.LOADER.load({
// We can define the child Worker's `env` to be
// any serializable object. Service stubs are
// serializable, so we'll pass in our stub.
env: {
CHAT_ROOM: chatRoom,
},
// Specify code and other options as usual...
compatibilityDate: "$today",
mainModule: "index.js",
modules: { "index.js": codeFromAgent },
globalOutbound: null,
});
return worker.getEntrypoint("Agent").run();

We have achieved an elegant sandbox:

  • The agent can only post to the desired room.
  • The posts are made using an API key, but the API key is never visible to the agent.
  • We rewrite the messages to include the agent's identity (this is just an example; we could perform any rewrite).
  • All this happens without any cooperation from the agent itself. It doesn't even know any of this is happening!

Tip: Tell your agent TypeScript types

In order for an AI agent to write code against your bindings, you have to tell it what interface they implement. The best way to do this is to give your agent TypeScript types describing the API, complete with comments documenting each declaration. Modern LLMs understand TypeScript well, having trained on a huge quantity of it, making it by far the most concise way to describe a JavaScript API. Note that even if the agent is actually writing plain JavaScript, you can still explain the interface to them using TypeScript.

Of course, you should declare your WorkerEntrypoint class to extend the TypeScript type, ensuring that it actually matches.

Passing normal Workers bindings

Sometimes, you may simply want to pass a standard Workers binding, like a KV namespace, R2 bucket, etc., into a Dynamic Worker. At this time, this is not directly supported. However, you can of course create a wrapper RPC interface, using the approach outlined above, which emulates a regular Workers binding, forwarding to a real binding in its implementation. Such a wrapper may even be preferable as it offers the opportunity to narrow the scope of the binding, filter or rewrite requests, etc. That said, in the future, we plan to support passing the bindings directly.