Configuration
This guide covers everything you need to configure agents for local development and production deployment, including Wrangler configuration file setup, type generation, environment variables, and the Cloudflare dashboard.
The typical file structure for an Agent project created from npm create cloudflare@latest agents-starter -- --template cloudflare/agents-starter follows:
Directorysrc/
- index.ts your Agent definition
Directorypublic/
- index.html
Directorytest/
- index.spec.ts your tests
- package.json
- tsconfig.json
- vitest.config.mts
- worker-configuration.d.ts
- wrangler.jsonc your Workers and Agent configuration
The wrangler.jsonc file configures your Cloudflare Worker and its bindings. Here is a complete example for an agents project:
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "my-agent-app", "main": "src/server.ts", // Set this to today's date "compatibility_date": "2026-07-20", "compatibility_flags": ["nodejs_compat"],
// Static assets (optional) "assets": { "directory": "public", "binding": "ASSETS", },
// Durable Object bindings for agents "durable_objects": { "bindings": [ { "name": "MyAgent", "class_name": "MyAgent", }, { "name": "ChatAgent", "class_name": "ChatAgent", }, ], },
// Provision storage for each agent class "exports": { "MyAgent": { "type": "durable-object", "storage": "sqlite", }, "ChatAgent": { "type": "durable-object", "storage": "sqlite", }, },
// AI binding (optional, for Workers AI) "ai": { "binding": "AI", },
// Observability (recommended) "observability": { "enabled": true, },}"$schema" = "node_modules/wrangler/config-schema.json"name = "my-agent-app"main = "src/server.ts"# Set this to today's datecompatibility_date = "2026-07-20"compatibility_flags = [ "nodejs_compat" ]
[assets]directory = "public"binding = "ASSETS"
[[durable_objects.bindings]]name = "MyAgent"class_name = "MyAgent"
[[durable_objects.bindings]]name = "ChatAgent"class_name = "ChatAgent"
[exports.MyAgent]type = "durable-object"storage = "sqlite"
[exports.ChatAgent]type = "durable-object"storage = "sqlite"
[ai]binding = "AI"
[observability]enabled = trueThe nodejs_compat flag is required for agents:
{ "compatibility_flags": ["nodejs_compat"],}compatibility_flags = [ "nodejs_compat" ]This enables Node.js compatibility mode, which agents depend on for crypto, streams, and other Node.js APIs.
Each agent class needs a binding:
{ "durable_objects": { "bindings": [ { "name": "Counter", "class_name": "Counter", }, ], },}[[durable_objects.bindings]]name = "Counter"class_name = "Counter"| Field | Description |
|---|---|
name | The property name on env. Use this in code: env.Counter |
class_name | Must match the exported class name exactly |
The exports field declares each Agent class your Worker exports and the storage backend Cloudflare should use for it:
{ "exports": { "MyAgent": { "type": "durable-object", "storage": "sqlite", }, },}[exports.MyAgent]type = "durable-object"storage = "sqlite"| Field | Description |
|---|---|
type | The kind of export. Always "durable-object" for an Agent. |
storage | The storage backend. Use "sqlite" for new Agents (recommended). |
For details on renaming, deleting, or transferring Agent classes, refer to Durable Object class exports. Existing Workers using the legacy migrations array continue to work — refer to Durable Object class migrations (legacy).
For serving static files (HTML, CSS, JS):
{ "assets": { "directory": "public", "binding": "ASSETS", },}[assets]directory = "public"binding = "ASSETS"With a binding, you can serve assets programmatically:
export default { async fetch(request, env) { // Static assets are served by the worker automatically by default
// Route the request to the appropriate agent const agentResponse = await routeAgentRequest(request, env); if (agentResponse) return agentResponse;
// Add your own routing logic here return new Response("Not found", { status: 404 }); },};export default { async fetch(request: Request, env: Env) { // Static assets are served by the worker automatically by default
// Route the request to the appropriate agent const agentResponse = await routeAgentRequest(request, env); if (agentResponse) return agentResponse;
// Add your own routing logic here return new Response("Not found", { status: 404 }); },} satisfies ExportedHandler<Env>;For Workers AI integration:
{ "ai": { "binding": "AI", },}[ai]binding = "AI"Access in your agent:
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", { prompt: "Hello!",});const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", { prompt: "Hello!",});The Agents SDK ships a shared tsconfig.json that sets all the compiler options needed for agents projects — including the ES2021 target required for @callable() decorators, strict mode, bundler module resolution, and Workers types.
Extend it in your tsconfig.json:
{ "extends": "agents/tsconfig"}This is equivalent to:
{ "compilerOptions": { "target": "ES2021", "lib": ["ES2022", "DOM", "DOM.Iterable"], "jsx": "react-jsx", "module": "ES2022", "moduleResolution": "bundler", "types": ["node", "@cloudflare/workers-types", "vite/client"], "allowImportingTsExtensions": true, "noEmit": true, "isolatedModules": true, "verbatimModuleSyntax": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }}You can override individual options as needed:
{ "extends": "agents/tsconfig", "compilerOptions": { "jsx": "preserve" }}The Agents SDK provides a Vite plugin that handles TC39 decorator transforms. Vite 8 uses Oxc for transpilation, which does not yet support TC39 decorators — without this plugin, @callable() and other decorators will fail at runtime.
Add the plugin to your vite.config.ts:
import { cloudflare } from "@cloudflare/vite-plugin";import react from "@vitejs/plugin-react";import agents from "agents/vite";import { defineConfig } from "vite";
export default defineConfig({ plugins: [agents(), react(), cloudflare()],});import { cloudflare } from "@cloudflare/vite-plugin";import react from "@vitejs/plugin-react";import agents from "agents/vite";import { defineConfig } from "vite";
export default defineConfig({ plugins: [agents(), react(), cloudflare()],});The agents() plugin is safe to include even if your project does not use decorators. It only runs the transform on files that contain @ syntax.
The starter template and all examples include this plugin by default. If you encounter SyntaxError: Invalid or unexpected token with decorators, refer to Callable methods — Troubleshooting.
Wrangler can generate TypeScript types for your bindings.
Run the types command:
npx wrangler typesThis creates or updates worker-configuration.d.ts with your Env type.
Specify a custom path:
npx wrangler types env.d.tsFor cleaner output (recommended for agents):
npx wrangler types env.d.ts --include-runtime falseThis generates just your bindings without Cloudflare runtime types.
// env.d.ts (generated)declare namespace Cloudflare { interface Env { OPENAI_API_KEY: string; Counter: DurableObjectNamespace; ChatAgent: DurableObjectNamespace; }}interface Env extends Cloudflare.Env {}You can also define types manually:
// env.d.ts// env.d.tsimport type { Counter } from "./src/agents/counter";import type { ChatAgent } from "./src/agents/chat";
interface Env { // Secrets OPENAI_API_KEY: string; WEBHOOK_SECRET: string;
// Agent bindings Counter: DurableObjectNamespace<Counter>; ChatAgent: DurableObjectNamespace<ChatAgent>;
// Other bindings AI: Ai; ASSETS: Fetcher; MY_KV: KVNamespace;}Add a script for easy regeneration:
{ "scripts": { "types": "wrangler types env.d.ts --include-runtime false" }}Create a .env file for local secrets (add to .gitignore):
# .envOPENAI_API_KEY=sk-...GITHUB_WEBHOOK_SECRET=whsec_...DATABASE_URL=postgres://...Access in your agent:
class MyAgent extends Agent { async onStart() { const apiKey = this.env.OPENAI_API_KEY; }}class MyAgent extends Agent { async onStart() { const apiKey = this.env.OPENAI_API_KEY; }}Use wrangler secret for production:
# Add a secretnpx wrangler secret put OPENAI_API_KEY# Enter value when prompted
# List secretsnpx wrangler secret list
# Delete a secretnpx wrangler secret delete OPENAI_API_KEYFor non-sensitive configuration, use vars in the Wrangler configuration file:
{ "vars": { "API_BASE_URL": "https://api.example.com", "MAX_RETRIES": "3", "DEBUG_MODE": "false", },}[vars]API_BASE_URL = "https://api.example.com"MAX_RETRIES = "3"DEBUG_MODE = "false"All values must be strings. Parse numbers and booleans in code:
const maxRetries = parseInt(this.env.MAX_RETRIES, 10);const debugMode = this.env.DEBUG_MODE === "true";const maxRetries = parseInt(this.env.MAX_RETRIES, 10);const debugMode = this.env.DEBUG_MODE === "true";Use env sections for different environments (for example, staging, production):
{ "name": "my-agent", "vars": { "API_URL": "https://api.example.com", },
"env": { "staging": { "vars": { "API_URL": "https://staging-api.example.com", }, }, "production": { "vars": { "API_URL": "https://api.example.com", }, }, },}name = "my-agent"
[vars]API_URL = "https://api.example.com"
[env.staging.vars]API_URL = "https://staging-api.example.com"
[env.production.vars]API_URL = "https://api.example.com"Deploy to specific environment:
npx wrangler deploy --env stagingnpx wrangler deploy --env productionWith Vite (recommended for full stack apps):
npx vite devWithout Vite:
npx wrangler devDurable Object state is persisted locally in .wrangler/state/:
Directory.wrangler/
Directorystate/
Directoryv3/
Directoryd1/
Directoryminiflare-D1DatabaseObject/
- ... (SQLite files)
To reset all local Durable Object state:
rm -rf .wrangler/stateOr restart with fresh state:
npx wrangler dev --persist-to=""You can inspect agent state directly:
# Find the SQLite filels .wrangler/state/v3/d1/
# Open with sqlite3sqlite3 .wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqliteWhen you deploy, Cloudflare automatically creates:
- Worker - Your deployed code
- Durable Object namespaces - One per agent class
- SQLite storage - Attached to each namespace
Log in to the Cloudflare dashboard, then go to Durable Objects.
Go to Durable ObjectsHere you can:
- See all Durable Object namespaces
- View individual object instances
- Inspect storage (keys and values)
- Delete objects
View live logs from your agents:
npx wrangler tailOr in the dashboard:
- Go to your Worker.
- Select the Observability tab.
- Enable real-time logs.
Filter by:
- Status (success, error)
- Search text
- Sampling rate
npx wrangler deployThis:
- Bundles your code
- Uploads to Cloudflare
- Provisions the Agent's Durable Object namespace storage
- Makes it live on
*.workers.dev
Add a route in the Wrangler configuration file:
{ "routes": [ { "pattern": "agents.example.com/*", "zone_name": "example.com", }, ],}[[routes]]pattern = "agents.example.com/*"zone_name = "example.com"Or use a custom domain (simpler):
{ "routes": [ { "pattern": "agents.example.com", "custom_domain": true, }, ],}[[routes]]pattern = "agents.example.com"custom_domain = trueDeploy without affecting production:
npx wrangler deploy --dry-run # See what would be uploadednpx wrangler versions upload # Upload new versionnpx wrangler versions deploy # Gradually roll outRoll back to a previous version:
npx wrangler rollbackDefine environments in the Wrangler configuration file:
{ "name": "my-agent", "main": "src/server.ts",
// Base configuration (shared) // Set this to today's date "compatibility_date": "2026-07-20", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }], }, "exports": { "MyAgent": { "type": "durable-object", "storage": "sqlite" }, },
// Environment overrides "env": { "staging": { "name": "my-agent-staging", "vars": { "ENVIRONMENT": "staging", }, }, "production": { "name": "my-agent-production", "vars": { "ENVIRONMENT": "production", }, }, },}name = "my-agent"main = "src/server.ts"# Set this to today's datecompatibility_date = "2026-07-20"compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]name = "MyAgent"class_name = "MyAgent"
[exports.MyAgent]type = "durable-object"storage = "sqlite"
[env.staging]name = "my-agent-staging"
[env.staging.vars] ENVIRONMENT = "staging"
[env.production]name = "my-agent-production"
[env.production.vars] ENVIRONMENT = "production"# Deploy to stagingnpx wrangler deploy --env staging
# Deploy to productionnpx wrangler deploy --env production
# Set secrets per environmentnpx wrangler secret put OPENAI_API_KEY --env stagingnpx wrangler secret put OPENAI_API_KEY --env productionEach environment gets its own Durable Objects. Staging agents do not share state with production agents.
To explicitly separate:
{ "env": { "staging": { "durable_objects": { "bindings": [ { "name": "MyAgent", "class_name": "MyAgent", "script_name": "my-agent-staging", }, ], }, }, },}[[env.staging.durable_objects.bindings]]name = "MyAgent"class_name = "MyAgent"script_name = "my-agent-staging"Each Agent maps to a Durable Object class. You manage the lifecycle of those classes (create, rename, delete, transfer) through the exports field of your Wrangler configuration file.
Declare the new class in the exports:
{ "exports": { "NewAgent": { "type": "durable-object", "storage": "sqlite" }, },}[exports.NewAgent]type = "durable-object"storage = "sqlite"Replace the entry for the old name with a renamed tombstone and add a live entry for the new name:
{ "exports": { "OldName": { "type": "durable-object", "state": "renamed", "renamed_to": "NewName", }, "NewName": { "type": "durable-object", "storage": "sqlite" }, },}[exports.OldName]type = "durable-object"state = "renamed"renamed_to = "NewName"
[exports.NewName]type = "durable-object"storage = "sqlite"Also update:
- The class name in code.
- The
class_namein bindings. - Export statements.
Replace the entry with a deleted tombstone:
{ "exports": { "AgentToKeep": { "type": "durable-object", "storage": "sqlite" }, "AgentToDelete": { "type": "durable-object", "state": "deleted" }, },}[exports.AgentToKeep]type = "durable-object"storage = "sqlite"
[exports.AgentToDelete]type = "durable-object"state = "deleted"- Keep
exportsin sync with your code. Every Agent class your Worker exports needs an entry. - Use tombstones, not silent removal. When retiring a class, leave a
deleted/renamed/transferredtombstone inexportsso Cloudflare reconciles the change explicitly. - Test locally first. Lifecycle changes apply on
wrangler deploy. - Back up production data before renaming or deleting.
Existing Workers using the legacy migrations array continue to work — refer to Durable Object class migrations (legacy) for the legacy reference, or Migrate from the legacy migrations flow to move to exports.
The class is missing from exports. Declare the class and its storage:
{ "exports": { "MissingClassName": { "type": "durable-object", "storage": "sqlite" }, },}[exports.MissingClassName]type = "durable-object"storage = "sqlite"Regenerate types:
npx wrangler types env.d.ts --include-runtime falseCheck that .env exists and contains the variable:
cat .env# Should show: MY_SECRET=valueIf your Worker uses the legacy migrations array, each entry must have a unique tag:
{ // Wrong - duplicate tags "migrations": [ { "tag": "v1", "new_sqlite_classes": ["A"] }, { "tag": "v1", "new_sqlite_classes": ["B"] }, ],}[[migrations]]tag = "v1"new_sqlite_classes = [ "A" ]
[[migrations]]tag = "v1"new_sqlite_classes = [ "B" ]{ // Correct - sequential tags "migrations": [ { "tag": "v1", "new_sqlite_classes": ["A"] }, { "tag": "v2", "new_sqlite_classes": ["B"] }, ],}[[migrations]]tag = "v1"new_sqlite_classes = [ "A" ]
[[migrations]]tag = "v2"new_sqlite_classes = [ "B" ]Consider converting to the declarative exports field.