An AI agent that writes JavaScript code and runs it in a sandboxed dynamic Worker isolate. The isolate has no internet access — its only connection to the outside world is a database binding that proxies through a sub-agent.
SandboxAgent (extends AIChatAgent)
│
├── executeCode tool ──▶ env.LOADER.get(id, {
│ mainModule: "harness.js",
│ modules: { "harness.js": ..., "user-code.js": agentCode },
│ env: { db: DatabaseLoopback }, ← only binding
│ globalOutbound: null, ← no fetch()
│ tails: [TailLoopback] ← capture console.log
│ })
│
├── DatabaseLoopback (WorkerEntrypoint)
│ └── proxies env.db calls back to the parent
│
├── TailLoopback (WorkerEntrypoint)
│ └── captures console output, delivers to parent
│
└── CustomerDatabase (Agent — own isolated SQLite)
└── query() / execute() / getAllCustomers()
Three layers of isolation:
- Dynamic isolate — code runs in a Worker with
globalOutbound: null. Nofetch(), noconnect(). - Restricted env — the only binding is
env.db, aDatabaseLoopbackthat proxies to the parent. - Sub-agent storage — the database is a child DO whose SQLite the parent can't access directly.
import { Agent } from "agents";
import { AIChatAgent } from "@cloudflare/ai-chat";
export class CustomerDatabase extends Agent<Env> {
onStart() {
this.sql`CREATE TABLE IF NOT EXISTS customers (...)`;
}
query(sql: string): Record<string, unknown>[] {
/* ... */
}
execute(sql: string): { success: boolean } {
/* ... */
}
}
export class SandboxAgent extends AIChatAgent<Env, SandboxState> {
private _db() {
return this.subAgent(CustomerDatabase, "database");
}
// DatabaseLoopback calls these — they forward to the sub-agent
async proxyDbQuery(sql: string) {
const db = await this._db();
return db.query(sql);
}
}The Loopback pattern: dynamic Worker isolates can't hold sub-agent stubs directly — they can only have ServiceStub bindings. So DatabaseLoopback (a WorkerEntrypoint) proxies calls back to the parent, which delegates to the sub-agent.
Chain: dynamic isolate → DatabaseLoopback → SandboxAgent → CustomerDatabase sub-agent
npm start- "Count customers by tier" — agent writes code, runs in sandbox, shows output
- "Find customers with emails containing 'example'" — agent queries via
env.db - "Add a new customer named Zara" — agent calls
env.db.execute()from sandbox - Check the Executions tab to see code + captured output
- Check the Customers tab to see the database state
- gadgets-subagents — fan-out/fan-in with parallel sub-agents
- gadgets-chat — multi-room chat via sub-agents
- gadgets-gatekeeper — gated database access via sub-agent boundary
- design/rfc-sub-agents.md — RFC for the sub-agent API