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.

  • Cloud Platforms
  • $cloudflare-workers
  • 206 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- cloudflare-workers

Installs to ~/.agents/skills/cloudflare-workers/ for every project on this machine.

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

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.

~/.agents/skills/cloudflare-workers/SKILL.md

--- name: cloudflare-workers description: Builds and deploys Cloudflare Workers with Wrangler - wrangler.jsonc configuration, compatibility dates and nodejs_compat, typed bindings generated… license: MIT ---

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

  1. Read AGENTS.md, package.json and the config file: wrangler.jsonc (default for new projects), wrangler.json or wrangler.toml. Keep whichever format the repo uses.
  2. 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_date to 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 repeat vars, KV, D1, R2 and other bindings, or that environment runs without them.
  • After any binding change run npx wrangler types to regenerate worker-configuration.d.ts (the Env interface 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

RuleWhy
No request-specific data in module-scope variablesOne isolate serves many concurrent requests; data leaks across users
Module scope may hold immutable config and lazily built clientsReused across requests on a warm isolate, never guaranteed
No I/O at module top levelGlobal 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 buffer128 MB memory per isolate; buffering a large body crashes it

Limits that shape design

LimitValue (check the current limits page before relying on it)
CPU time per request10 ms Free; Paid defaults to 30 s, raise via "limits": { "cpu_ms": 300000 } (5 min max)
Memory128 MB per isolate
Subrequests per invocation50 on Free, much higher on Paid
Compressed script size3 MB Free, 10 MB Paid
Wall timeUnlimited 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.
  • Response bodies 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 dev local KV/D1 state is not production data; --remote touches real data.
  • cache.put via 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 dist builds and reports the bundle size.
  • npx vitest run passes in the Workers pool.
  • After deploy, curl -i https://api.example.com/healthz returns 200, and wrangler tail shows 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.yamlgenerated from this page

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.

esc