Cloudflare Workers skill for Codex
Edge code that respects the isolate model. Codex sets up Workers with a pinned compatibility date, bindings repeated per environment, and Env types generated by wrangler types instead of hand-written. Code avoids per-request globals and module-scope I/O, pushes post-response work into waitUntil, tests run in the Workers Vitest pool, and production deploys go through versioned gradual rollouts with a known rollback.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- cloudflare-workers Installs to ~/.agents/skills/cloudflare-workers/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install cloudflare-workers The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project cloudflare-workers Installs to .agents/skills/cloudflare-workers/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/cloudflare-workers.zipunzip -o cloudflare-workers.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) cloudflare-workers PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $cloudflare-workers, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and deploys Cloudflare Workers with Wrangler - wrangler.jsonc configuration, compatibility dates and nodejs_compat, typed bindings generated by wrangler types (KV, D1, R2, Durable Objects, Queues, service bindings, secrets), ctx.waitUntil, cron triggers, static assets, environments, gradual deployments and rollbacks, CPU and memory limits, and Vitest tests in the Workers runtime. Use when writing or deploying a Worker, adding a binding, hitting CPU time or subrequest limits, porting Node code to the edge, or setting up staging and production Workers.
Use it when
- Writing, configuring or deploying a Cloudflare Worker
- Adding KV, D1, R2, Queue or service bindings
- CPU time, memory or subrequest limits are being hit
- Porting a Node.js handler or library to the Workers runtime
Use something else when
- The project is a static site with optional functions on Pages: use cloudflare-pages
- You need coordinated per-key state or WebSocket rooms: use cloudflare-durable-objects
- You are designing the routing layer with a framework: use hono-edge-api
How to check the result
A dry-run build reports bundle size, Vitest passes in the Workers pool, and after deploy the health route returns 200 with no exceptions in wrangler tail.
The mistake it prevents
Defining bindings only at the top level of the config, so the staging environment deploys with no database or KV attached.
Done well means: the Worker’s config is in version control with a pinned compatibility
date, binding types are generated rather than hand-written, secrets live in Wrangler
secrets (and .dev.vars locally, gitignored), the code fits the isolate model (no
per-request state in globals, no blocking work after the response without waitUntil), and
tests run in the real Workers runtime.
Inspect first
- Read
AGENTS.md,package.jsonand the config file:wrangler.jsonc(default for new projects),wrangler.jsonorwrangler.toml. Keep whichever format the repo uses. - Check Wrangler is local to the project and current major (v4 at the time of writing):
npx wrangler --version
npx wrangler whoami # needs network and a login or CLOUDFLARE_API_TOKEN
npx wrangler deployments list # what is live now
Prefer npx wrangler (project-pinned) over a global install. New projects:
npm create cloudflare@latest (needs network).
Config skeleton
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "shop-api",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"compatibility_flags": ["nodejs_compat"],
"observability": { "enabled": true },
"vars": { "LOG_LEVEL": "info" },
"kv_namespaces": [{ "binding": "CACHE", "id": "<kv-id>" }],
"d1_databases": [{ "binding": "DB", "database_name": "shop", "database_id": "<d1-id>" }],
"r2_buckets": [{ "binding": "ASSETS_BUCKET", "bucket_name": "shop-assets" }],
"services": [{ "binding": "AUTH", "service": "auth-worker" }],
"triggers": { "crons": ["*/10 * * * *"] },
"routes": [{ "pattern": "api.example.com", "custom_domain": true }],
"env": {
"staging": {
"name": "shop-api-staging",
"vars": { "LOG_LEVEL": "debug" },
"d1_databases": [{ "binding": "DB", "database_name": "shop-staging", "database_id": "<id>" }]
}
}
}
- Set
compatibility_dateto the date you test against and bump it deliberately; behaviour changes are gated on it. - Bindings are not inherited by environments. Every
env.<name>block must repeatvars, KV, D1, R2 and other bindings, or that environment runs without them. - After any binding change run
npx wrangler typesto regenerateworker-configuration.d.ts(theEnvinterface and runtime types). Commit it or generate it in CI, as the repo prefers.
Handler shape
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/healthz") return new Response("ok");
if (url.pathname.startsWith("/products/")) {
const id = url.pathname.split("/")[2];
const cached = await env.CACHE.get(`product:${id}`, "json");
if (cached) return Response.json(cached);
const row = await env.DB.prepare("SELECT * FROM products WHERE id = ?1").bind(id).first();
if (!row) return new Response("not found", { status: 404 });
ctx.waitUntil(env.CACHE.put(`product:${id}`, JSON.stringify(row), { expirationTtl: 300 }));
return Response.json(row);
}
return new Response("not found", { status: 404 });
},
async scheduled(controller, env, ctx) {
ctx.waitUntil(purgeExpired(env.DB));
},
} satisfies ExportedHandler<Env>;
For more than a handful of routes, use Hono (hono-edge-api) rather than hand-rolled
if chains.
The isolate model
| Rule | Why |
|---|---|
| No request-specific data in module-scope variables | One isolate serves many concurrent requests; data leaks across users |
| Module scope may hold immutable config and lazily built clients | Reused across requests on a warm isolate, never guaranteed |
| No I/O at module top level | Global scope cannot make subrequests; it fails at deploy or first request |
Post-response work goes in ctx.waitUntil() | Otherwise it is cancelled when the response is sent |
| Streams: return them, do not buffer | 128 MB memory per isolate; buffering a large body crashes it |
Limits that shape design
| Limit | Value (check the current limits page before relying on it) |
|---|---|
| CPU time per request | 10 ms Free; Paid defaults to 30 s, raise via "limits": { "cpu_ms": 300000 } (5 min max) |
| Memory | 128 MB per isolate |
| Subrequests per invocation | 50 on Free, much higher on Paid |
| Compressed script size | 3 MB Free, 10 MB Paid |
| Wall time | Unlimited while the client stays connected; CPU is what is metered |
CPU time excludes time spent awaiting fetches or bindings. When exceededCpu errors appear,
profile with npx wrangler dev and the DevTools profiler, move heavy parsing out of the
hot path, or push the work to a Queue consumer or Workflow.
Secrets
npx wrangler secret put STRIPE_KEY # prompts for the value; never pass it on the command line
npx wrangler secret put STRIPE_KEY --env staging
npx wrangler secret list
Locally, put secrets in .dev.vars (dotenv format) and make sure it is in .gitignore.
Never put secrets in vars; they are visible in the dashboard and in the config.
Node.js compatibility
nodejs_compat provides node:buffer, node:crypto, node:stream, AsyncLocalStorage
and many more, plus polyfills for parts of fs, net, http. Native addons, child
processes and anything needing a real filesystem do not work. Before porting a library,
check whether it has a workerd or browser export condition; bundling the Node build often
fails at runtime, not at build time.
Local dev and tests
npx wrangler dev # local workerd with local KV/D1/R2 state in .wrangler/
npx wrangler dev --remote # runs on Cloudflare's network with real bindings (careful)
Vitest in the Workers runtime:
// vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({
test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.jsonc" } } } },
});
import { env, SELF } from "cloudflare:test";
import { it, expect } from "vitest";
it("returns 404 for unknown product", async () => {
const res = await SELF.fetch("https://example.com/products/missing");
expect(res.status).toBe(404);
});
Each test gets isolated storage, so seed D1 or KV inside the test.
Deploy, gradual rollout, rollback
npx wrangler deploy --env staging
npx wrangler deploy # production: confirm with the user first
npx wrangler versions upload # upload without routing traffic
npx wrangler versions deploy # interactive: split traffic, e.g. 10% new / 90% old
npx wrangler rollback # previous version; confirm first
npx wrangler tail shop-api --format=pretty # live logs
In CI use a scoped API token (CLOUDFLARE_API_TOKEN, Workers Scripts Edit plus the binding
permissions it needs) and CLOUDFLARE_ACCOUNT_ID; never a Global API Key.
Gotchas
fetch()to another Worker on the same zone via its public hostname can loop or be blocked; use a service binding instead.Responsebodies can be read once;.clone()before reading if you also return it.- Cron triggers run in UTC and fire on every deployed environment that declares them.
wrangler devlocal KV/D1 state is not production data;--remotetouches real data.cache.putvia the Cache API is per data center, not global.- A deployment from the dashboard editor is overwritten by the next
wrangler deploy.
Verification
npx wrangler deploy --dry-run --outdir distbuilds and reports the bundle size.npx vitest runpasses in the Workers pool.- After deploy,
curl -i https://api.example.com/healthzreturns 200, andwrangler tailshows no exceptions for a smoke test.
Report template
**Worker:** shop-api (compat 2026-10-01, nodejs_compat), routes api.example.com
**Bindings:** CACHE (KV), DB (D1 shop), ASSETS_BUCKET (R2), AUTH (service) - types regenerated
**Secrets:** STRIPE_KEY (prod, staging); .dev.vars gitignored
**Rollout:** versions upload -> 10% for 30 min -> 100%; rollback with wrangler rollback
**Checks:** vitest 24 passed, bundle 412 KiB gzip, smoke test 200, no exceptions in tail What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Cloudflare Workers"
short_description: "Edge code that respects the isolate model"
default_prompt: "$cloudflare-workers Create a Cloudflare Worker API with D1 and KV bindings and a staging environment" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.