A few weeks ago, we announced Dynamic Workers, a new feature of the Workers platform which lets you load Worker code on-the-fly into a secure sandbox. The Dynamic Worker Loader API essentially provides direct access to the basic compute isolation primitive that Workers has been based on all along: isolates, not containers. Isolates are much lighter-weight than containers, and as such, can load 100x faster using 1/10 the memory. They are so efficient, they can be treated as "disposable": start one up to run a few lines of code, then throw it away. Like a secure version of eval().
Dynamic Workers have many uses. In the original announcement, we focused on how to use them to run AI-agent-generated code as an alternative to tool calls. In this use case, an AI agent performs actions at the request of a user by writing a few lines of code and executing them. The code is single-use, intended to perform one task one time, and is thrown away immediately after it executes.
But what if you want an AI to generate more persistent code? What if you want your AI to build a small application with a custom UI the user can interact with? What if you want that application to have long-lived state? But of course, you still want it to run in a secure sandbox.
One way to do this would be to use Dynamic Workers, and simply provide the Worker with an RPC API that gives it access to storage. Using bindings, you could give the Dynamic Worker an API that points back to your remote SQL database (perhaps backed by Cloudflare D1, or a Postgres database you access through Hyperdrive — it's up to you).
But Workers also has a unique and extremely fast type of storage that may be a perfect fit for this use case: Durable Objects. A Durable Object is a special kind of Worker that has a unique name, with one instance globally per name. That instance has a SQLite database attached, which lives on local disk on the machine where the Durable Object runs. This makes storage access ridiculously fast: there is effectively zero latency.
Perhaps, then, what you really want is for your AI to write code for a Durable Object, and then you want to run that code in a Dynamic Worker.
But how?
This presents a weird problem. Normally, to use Durable Objects you have to:
-
Write a class extending
DurableObject. -
Export it from your Worker's main module.
-
Specify in your Wrangler config that storage should be provision for this class. This creates a Durable Object namespace that points at your class for handling incoming requests.
-
Declare a Durable Object namespace binding pointing at your namespace (or use ctx.exports), and use it to make requests to your Durable Object.
This doesn't extend naturally to Dynamic Workers. First, there is the obvious problem: The code is dynamic. You run it without invoking the Cloudflare API at all. But Durable Object storage has to be provisioned through the API, and the namespace has to point at an implementing class. It can't point at your Dynamic Worker.
But there is a deeper problem: Even if you could somehow configure a Durable Object namespace to point directly at a Dynamic Worker, would you want to? Do you want your agent (or user) to be able to create a whole namespace full of Durable Objects? To use unlimited storage spread around the world?
You probably don't. You probably want some control. You may want to limit, or at least track, how many objects they create. Maybe you want to limit them to just one object (probably good enough for vibe-coded personal apps). You may want to add logging and other observability. Metrics. Billing. Etc.
To do all this, what you really want is for requests to these Durable Objects to go to your code first, where you can then do all the "logistics", and then forward the request into the agent's code. You want to write a supervisor that runs as part of every Durable Object.
Solution: Durable Object Facets
Today we are releasing, in open beta, a feature that solves this problem.
Durable Object Facets allow you to load and instantiate a Durable Object class dynamically, while providing it with a SQLite database to use for storage. With Facets:
-
First you create a normal Durable Object namespace, pointing to a class you write.
-
In that class, you load the agent's code as a Dynamic Worker, and call into it.
-
The Dynamic Worker's code can implement a Durable Object class directly. That is, it literally exports a class declared as
extends DurableObject. -
You are instantiating that class as a "facet" of your own Durable Object.
-
The facet gets its own SQLite database, which it can use via the normal Durable Object storage APIs. This database is separate from the supervisor's database, but the two are stored together as part of the same overall Durable Object.
How it works
Here is a simple, complete implementation of an app platform that dynamically loads and runs a Durable Object class:
import { DurableObject } from "cloudflare:workers";
// Для целей этого примера мы будем использовать этот статический
// код приложения, но в реальном мире он может быть сгенерирован
// ИИ (или, возможно, даже человеком).
const AGENT_CODE = `
import { DurableObject } from "cloudflare:workers";
// Простое приложение, которое запоминает, сколько раз его вызывали,
// и возвращает это число.
export class App extends DurableObject {
fetch(request) {
// Мы используем storage.kv здесь для простоты, но storage.sql
// также доступен. Оба варианта используют SQLite в качестве бэкенда.
let counter = this.ctx.storage.kv.get("counter") || 0;
++counter;
this.ctx.storage.kv.put("counter", counter);
return new Response("Вы сделали " + counter + " запрос(ов).\n");
}
}
`;
// AppRunner — это Durable Object, который вы пишете и который отвечает за
// динамическую загрузку приложений и передачу им запросов.
// Каждый экземпляр AppRunner содержит разное приложение.
export class AppRunner extends DurableObject {
async fetch(request) {
// Мы получили HTTP-запрос, который хотим перенаправить
// в приложение.
// Само приложение работает как дочерний фасет с именем "app". Один Durable
// Object может иметь любое количество фасетов (в пределах лимитов хранилища)
// с разными именами, но в данном случае у нас только один. Вызовите
// this.ctx.facets.get(), чтобы получить заглушку, указывающую на него.
let facet = this.ctx.facets.get("app", async () => {
// Если этот колбэк вызван, это означает, что фасет ещё не
// запущен (или находится в гибернации). В этом колбэке мы можем
// сообщить системе, какой код мы хотим загрузить.
// Загружаем Dynamic Worker.
let worker = this.#loadDynamicWorker();
// Получаем экспортированный класс, который нас интересует.
let appClass = worker.getDurableObjectClass("App");
return { class: appClass };
});
// Перенаправляем запрос фасету.
// (В качестве альтернативы здесь можно вызывать RPC-методы.)
return await facet.fetch(request);
}
// RPC-метод, который может вызвать клиент, чтобы установить динамический код
// для этого приложения.
setCode(code) {
// Сохраняем код в SQLite-хранилище AppRunner.
// Каждый уникальный код должен иметь уникальный ID для передачи в
// API загрузчика Dynamic Worker, поэтому мы генерируем его случайным образом.
this.ctx.storage.kv.put("codeId", crypto.randomUUID());
this.ctx.storage.kv.put("code", code);
}
#loadDynamicWorker() {
// Используем API загрузчика Dynamic Worker как обычно. Используем get()
// вместо load(), так как мы можем загружать одного и того же Worker много раз.
let codeId = this.ctx.storage.kv.get("codeId");
return this.env.LOADER.get(codeId, async () => {
// Этот Worker ещё не был загружен. Загружаем его код из
// нашего собственного хранилища.
let code = this.ctx.storage.kv.get("code");
return {
compatibilityDate: "2026-04-01",
mainModule: "worker.js",
modules: { "worker.js": code },
globalOutbound: null, // блокируем доступ к сети
}
});
}
}
// Это простой HTTP-обработчик Workers, который использует AppRunner.
export default {
async fetch(req, env, ctx) {
// Получаем экземпляр AppRunner с именем "my-app".
// (Каждое имя имеет ровно один экземпляр Durable Object в мире.)
let obj = ctx.exports.AppRunner.getByName("my-app");
// Инициализируем его кодом. (В реальном сценарии вы бы захотели
// вызвать это только один раз, а не при каждом запросе.)
await obj.setCode(AGENT_CODE);
// Перенаправляем запрос ему.
return await obj.fetch(req);
}
}
В этом примере:
-
AppRunner— это "обычный" Durable Object, написанный разработчиком платформы (вами). -
Каждый экземпляр
AppRunnerуправляет одним приложением. Он хранит код приложения и загружает его по требованию. -
Само приложение реализует и экспортирует класс Durable Object, который, как ожидает платформа, называется
App. -
AppRunnerзагружает код приложения с помощью Dynamic Workers, а затем выполняет его как Durable Object Facet (фасет). -
Каждый экземпляр
AppRunner— это один Durable Object, состоящий из двух баз данных SQLite: одна принадлежит родителю (самомуAppRunner), а другая — фасету (App). Эти базы данных изолированы: приложение не может читать базу данныхAppRunner, только свою собственную.
Чтобы запустить пример, скопируйте код выше в файл worker.js, соедините его со следующим wrangler.jsonc и запустите локально с помощью npx wrangler dev.
// wrangler.jsonc для приведенного выше примера worker.
{
"compatibility_date": "2026-04-01",
"main": "worker.js",
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"AppRunner"
]
}
],
"worker_loaders": [
{
"binding": "LOADER",
},
],
}
Начать разработку
Фасеты (Facets) — это функция Dynamic Workers, доступная в бета-версии сразу же пользователям платного тарифа Workers Paid plan.