Skip to content

Cloudflare DO Minimal App

Build the smallest Cloudflare Durable Object app that uses the agentOS facade, symbolic LLM material, and one tool.

One Worker module exporting an AgentDO class and a fetch handler. The DO uses defineAgentDO, one echo tool, one symbolic endpoint, one symbolic credential, and one openAIChat route.

  1. Install the app-facing packages and peers:

    Terminal window
    bun add @agent-os/backend-cloudflare-do @agent-os/kernel effect
    bun add -d typescript @cloudflare/workers-types
  2. Define one tool:

    import { defineTool } from "@agent-os/kernel";
    import { Schema } from "effect";
    const echo = defineTool({
    name: "echo",
    description: "Return the supplied text.",
    args: Schema.Struct({ text: Schema.String }),
    authority: "tutorial.local",
    admit: () => ({ ok: true }),
    execute: ({ text }) => ({ text }),
    });
  3. Define the Durable Object facade:

    import {
    credential,
    defineAgentDO,
    endpoint,
    openAIChat,
    type CloudflareAgentEnv,
    } from "@agent-os/backend-cloudflare-do";
    interface MaterialEnv extends CloudflareAgentEnv {
    readonly OPENAI_BASE_URL: string;
    readonly OPENAI_API_KEY: string;
    }
    export const AgentDO = defineAgentDO<MaterialEnv>({
    bindings: [
    endpoint<MaterialEnv>("llm").from((env) => env.OPENAI_BASE_URL),
    credential<MaterialEnv>("llm-key", { provider: "openai", purpose: "chat" }).from(
    (env) => env.OPENAI_API_KEY,
    ),
    ],
    llms: {
    default: openAIChat({
    model: "gpt-4.1-mini",
    endpoint: "llm",
    credential: "llm-key",
    }),
    },
    tools: [echo],
    scopeRefForScope: (scope) => ({ kind: "conversation", scopeId: scope }),
    });
  4. In the Worker fetch handler, choose a scope and call the DO stub:

    type AgentDOInstance = InstanceType<typeof AgentDO>;
    interface WorkerEnv extends MaterialEnv {
    readonly AGENT_DO: DurableObjectNamespace<AgentDOInstance>;
    }
    export default {
    async fetch(request: Request, env: WorkerEnv) {
    const id = env.AGENT_DO.idFromName("tutorial");
    const agent = env.AGENT_DO.get(id);
    return Response.json(
    await agent.submit({
    intent: "Echo hello",
    input: { text: "hello" },
    deliver: "tutorial.echo.ready",
    }),
    );
    },
    };
  5. Add Cloudflare Durable Object binding and migration config before deploying. Keep provider URLs and secrets in environment bindings, not source code.

  6. Run local checks before live deployment:

    Terminal window
    bunx tsc -p tsconfig.json
    bun build src/worker.ts --target=browser --outdir dist --external cloudflare:workers

The app typechecks and bundles while importing only public package entrypoints. This is a local proof. It does not prove a live Cloudflare deployment or a live LLM call.

Bind provider material with provider material binding, or install the same app-facing packages with internal npm consumer app.