This guide covers everything you need to configure agents for local development and production deployment, including wrangler.jsonc setup, type generation, environment variables, and the Cloudflare dashboard.
The wrangler.jsonc file configures your Cloudflare Worker and its bindings. Here's a complete example for an agents project:
The nodejs_compat flag is required for agents:
"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", // Property name on `env` (env.Counter)
"class_name": "Counter" // Exported class name (must match exactly)
}
]
}| Field | Description |
|---|---|
name |
The property name on env. Use this in code: env.Counter |
class_name |
Must match the exported class name exactly |
When name and class_name differ:
{
"name": "COUNTER_DO", // env.COUNTER_DO
"class_name": "CounterAgent" // export class CounterAgent
}This is useful when you want environment variable-style naming (COUNTER_DO) but more descriptive class names (CounterAgent).
Migrations tell Cloudflare how to set up storage for your Durable Objects:
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyAgent"]
}
]| Field | Description |
|---|---|
tag |
Version identifier (e.g., "v1", "v2"). Must be unique |
new_sqlite_classes |
Agent classes that use SQLite storage (state persistence) |
deleted_classes |
Classes being removed |
renamed_classes |
Classes being renamed (see Migrations below) |
For serving static files (HTML, CSS, JS):
"assets": {
"directory": "public", // Folder containing static files
"binding": "ASSETS" // Optional: binding for programmatic access
}With a binding, you can serve assets programmatically:
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 if you want to handle requests that are not for agents
return new Response("Not found", { status: 404 });
}
};For Workers AI integration:
"ai": {
"binding": "AI",
"remote": true // Mandatory: use remote inference (for local dev)
}Access in your agent:
const response = await this.env.AI.run("@cf/moonshotai/kimi-k2.5", {
prompt: "Hello!"
});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<import("./src/server").Counter>;
ChatAgent: DurableObjectNamespace<import("./src/server").ChatAgent>;
}
}
interface Env extends Cloudflare.Env {}You can also define types manually:
// env.d.ts
import 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):
# .env
OPENAI_API_KEY=sk-...
GITHUB_WEBHOOK_SECRET=whsec_...
DATABASE_URL=postgres://...Access in your agent:
class MyAgent extends Agent<Env> {
async onStart() {
const apiKey = process.env.OPENAI_API_KEY;
}
}Use wrangler secret for production:
# Add a secret
wrangler secret put OPENAI_API_KEY
# Enter value when prompted
# List secrets
wrangler secret list
# Delete a secret
wrangler secret delete OPENAI_API_KEYFor non-sensitive configuration, use vars in wrangler.jsonc:
{
"vars": {
"API_BASE_URL": "https://api.example.com",
"MAX_RETRIES": "3",
"DEBUG_MODE": "false"
}
}Note: All values must be strings. Parse numbers/booleans in code:
const maxRetries = parseInt(process.env.MAX_RETRIES, 10);
const debugMode = process.env.DEBUG_MODE === "true";Use [env.{name}] sections for different environments (e.g. 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"
}
}
}
}Deploy to specific environment:
wrangler deploy --env staging
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/:
.wrangler/
└── state/
└── v3/
└── d1/
└── miniflare-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 file
ls .wrangler/state/v3/d1/
# Open with sqlite3
sqlite3 .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
- Go to dash.cloudflare.com
- Select your account → Workers & Pages
- Click your Worker
- Go to Durable Objects tab
Here 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
- Click Logs tab
- Enable real-time logs
Filter by:
- Status (success, error)
- Search text
- Sampling rate
The dashboard shows:
- Request count
- Error rate
- CPU time
- Duration percentiles
- Durable Object metrics
npx wrangler deployThis:
- Bundles your code
- Uploads to Cloudflare
- Applies migrations
- Makes it live on
*.workers.dev
Add a route in wrangler.jsonc:
{
"routes": [
{
"pattern": "agents.example.com/*",
"zone_name": "example.com"
}
]
}Or use a custom domain (simpler):
{
"routes": [
{
"pattern": "agents.example.com",
"custom_domain": true
}
]
}Deploy without affecting production:
npx wrangler deploy --dry-run # See what would be uploaded
npx wrangler versions upload # Upload new version
npx wrangler versions deploy # Gradually roll outRoll back to a previous version:
npx wrangler rollbackDefine environments in wrangler.jsonc:
{
"name": "my-agent",
"main": "src/server.ts",
// Base configuration (shared)
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }],
// Environment overrides
"env": {
"staging": {
"name": "my-agent-staging",
"vars": {
"ENVIRONMENT": "staging"
}
},
"production": {
"name": "my-agent-production",
"vars": {
"ENVIRONMENT": "production"
}
}
}
}# Deploy to staging
npx wrangler deploy --env staging
# Deploy to production
npx wrangler deploy --env production
# Set secrets per environment
npx wrangler secret put OPENAI_API_KEY --env staging
npx wrangler secret put OPENAI_API_KEY --env productionEach environment gets its own Durable Objects. Staging agents don't share state with production agents.
To explicitly separate:
{
"env": {
"staging": {
"durable_objects": {
"bindings": [
{
"name": "MyAgent",
"class_name": "MyAgent",
"script_name": "my-agent-staging" // Different namespace
}
]
}
}
}
}Migrations manage Durable Object storage schema changes.
Add to new_sqlite_classes in a new migration:
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["ExistingAgent"]
},
{
"tag": "v2",
"new_sqlite_classes": ["NewAgent"]
}
]Use renamed_classes:
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["OldName"]
},
{
"tag": "v2",
"renamed_classes": [
{
"from": "OldName",
"to": "NewName"
}
]
}
]Important: Also update:
- The class name in code
- The
class_namein bindings - Export statements
Use deleted_classes:
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["AgentToDelete", "AgentToKeep"]
},
{
"tag": "v2",
"deleted_classes": ["AgentToDelete"]
}
]Warning: This permanently deletes all data for that class.
- Never modify existing migrations - Always add new ones
- Use sequential tags - v1, v2, v3 (or use dates: 2025-01-15)
- Test locally first - Migrations run on deploy
- Back up production data - Before renaming or deleting
The class isn't in migrations:
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MissingClassName"] // Add it here
}
]Regenerate types:
npx wrangler types env.d.ts --include-runtime falseCheck that .env exists and contains the variable:
cat .env
# Should show: MY_SECRET=valueMigration tags must be unique. If you see conflicts:
// Wrong - duplicate tags
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["A"] },
{ "tag": "v1", "new_sqlite_classes": ["B"] } // Error!
]
// Correct - sequential tags
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["A"] },
{ "tag": "v2", "new_sqlite_classes": ["B"] }
]
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "my-agent-app", "main": "src/server.ts", "compatibility_date": "2025-01-01", "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" } ] }, // Required: Enable SQLite storage for agents "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MyAgent", "ChatAgent"] } ], // AI binding (optional, for Workers AI) "ai": { "binding": "AI" } }