Skip to content

Checks and diagnostics

Before you ship, ask:

$ pnpm infra check example/guestbook/app.ts
✓ guestbook: no problems found (app, types, bundle)

That’s what you want to see. Here’s what you’d rather see now than in production. The fixture test/fixtures/diagnostics has one of each problem the offline checks find:

$ pnpm infra check test/fixtures/diagnostics/app.ts
error used-undeclared test/fixtures/diagnostics/api.ts:16:133
the Worker uses Db, which it doesn't bind
fix: bind it: .bind(…, Db)
error compatibility-date-future test/fixtures/diagnostics/api.ts:16:64
the Worker api's compatibility date, 2099-01-01, is after today's, 2026-10-17
fix: set compatibilityDate: '2026-10-17', or an earlier date
error cron-invalid test/fixtures/diagnostics/api.ts:16:98
the Worker api's cron "61 * * * *": its minute has "61"; it takes 0 to 59
fix: write it as five fields, such as '*/5 * * * *' for every five minutes
error name-too-long test/fixtures/diagnostics/rooms.ts:13:28
the Worker rooms-with-a-name-long-enough is named diagnostics-fixture-with-long-name-rooms-with-a-name-long-enough in the account, 64 characters; a Worker's name is at most 63
fix: shorten the app's name (diagnostics-fixture-with-long-name) or the Worker's (rooms-with-a-name-long-enough) by 1 character
warning queue-no-consumer test/fixtures/diagnostics/api.ts:16:28
the queue jobs has producers (the Worker api) and no consumer, so its messages wait until they expire
fix: consume it in one Worker: queues: [Jobs.consume(handler)]
warning declared-unused test/fixtures/diagnostics/idle.ts:7:39
the Worker binds Db, and its code never uses it
fix: drop Db from its .bind(…), or use it
warning handler-missing test/fixtures/diagnostics/idle.ts:7:28
the Worker idle has no handlers, so it answers nothing
fix: give it a fetch, an HTTP app, a cron, a queue, a contract, a Durable Object, a workflow or a tail handler, or remove it from the app
warning migration-destructive test/fixtures/diagnostics/rooms.ts:10:65
a migration of the Durable Object ledger drops data: DROP TABLE entries
fix: make sure it's meant: it runs once on every object, the first time each starts on the new version
diagnostics-fixture-with-long-name: 4 errors, 4 warnings (app, types, bundle); `infra explain <code>` says more

Every problem has the same shape: how bad it is, a stable code, the file, line and column, what’s wrong in a sentence, and the fix. It exits 1 when there’s an error, so it fits in CI as it is. That run took about five seconds.

Offline, with no account and no token:

  • Types. The TypeScript checker, on the app’s project (the nearest tsconfig.json). forge-effect’s own compile errors get their own codes: the compiler’s 'missing bindings' is used-undeclared, and so on. Anything else TypeScript says is type-error. In the same run, it asks the checker which resources each Worker binds but never uses (declared-unused): a Worker’s type records what its code needs, so this falls out for free.
  • Declarations. Names over Cloudflare’s limits, a compatibility date in the future, a cron Cloudflare won’t take, a Worker with no handlers, a queue no Worker consumes, a migration that drops data, a route that isn’t one, a variable its schema refuses, and the app’s own mistakes (a resource declared twice, a contract no Worker implements, a Worker importing another’s module), each at the declaration.
  • Bundles. Each Worker bundled as it would deploy: a failure, and its size against Cloudflare’s limits (10 MB gzipped; over 3 MB, the free plan’s, is a warning, as the check can’t know your plan), and bindings ship can’t deploy yet.

--no-types and --no-bundles skip those two, when you want it fast. --env <name> checks an environment’s values (a variable chosen by a function, say, or routes).

--account adds what plan would read, as diagnostics. It needs CLOUDFLARE_API_TOKEN, and reads only.

  • names taken by something that isn’t the app’s (name-taken),
  • resources deleted outside the tool, and Workers running a version it didn’t deploy (drift),
  • orphans waiting for prune (orphaned),
  • migrations the next ship applies (migration-pending),
  • contract changes that would break deployed callers,
  • secrets declared with no value (secret-missing), with the command that sets one,
  • Durable Object classes that change, so the ship needs --atomic (exports-changed),
  • routes and domains in no zone of the account (zone-missing), or serving another Worker (route-taken),
  • what your plan limits, such as cron triggers (plan-limit),
  • and a call the token isn’t allowed to make (token-scope-missing).

Each code means one thing, always. Ask about one:

$ pnpm infra explain used-undeclared
used-undeclared (error): the code uses a resource or contract the Worker does not bind
A handler, or a Layer, uses a resource (a KV namespace, a D1 database, a queue, an object contract, another Worker's contract) that the Worker doesn't bind, so it would fail at runtime with a missing binding. Bind it: `.bind(..., Db)`. The compiler reports it as `'missing bindings'`.

With no code, it lists them all, with their severity (trimmed here; there are 57):

$ pnpm infra explain
usage: infra explain <code>; the codes:
used-undeclared error the code uses a resource or contract the Worker does not bind
object-service-outside error a Durable Object's own service is used outside an object
contract-unimplemented error a Worker calls a contract no Worker of the app implements
…
declared-unused warning a Worker binds a resource its code never uses
…
name-taken error a resource's name exists in the account, and isn't this app's
secret-missing error a Worker declares a secret that has no value
…
plan-stale error a saved plan is out of date
orphaned info a resource is no longer declared, and kept until prune
…

--json prints the same as a report, for a program, or an AI agent, to read:

$ pnpm infra check test/fixtures/diagnostics/app.ts --json --no-types --no-bundles
{
"version": 1,
"app": "diagnostics-fixture-with-long-name",
"env": "production",
"checks": [
"app"
],
"errors": 3,
"warnings": 3,
"diagnostics": [
{
"code": "compatibility-date-future",
"severity": "error",
"message": "the Worker api's compatibility date, 2099-01-01, is after today's, 2026-10-17",
"source": "app",
"location": {
"file": "test/fixtures/diagnostics/api.ts",
"line": 16,
"column": 64,
"worker": "api"
},
"fix": {
"description": "set compatibilityDate: '2026-10-17', or an earlier date"
}
},
…

version: 1 is a promise: fields are added, never renamed. A fix can carry a command too, such as the infra secret put that sets a missing secret.

Two more commands report the same way:

  • infra graph --json: what the app declares, read with no requests. Each Worker’s name in the account, handlers, resources with their bindings and names, crons, routes, Tail Workers, and the order they deploy in. It’s how an agent checks that the app it wrote is the app the tool sees:

    $ pnpm infra graph example/guestbook/app.ts --json
    {
    "version": 1,
    "app": "guestbook",
    "order": [
    "guestbook"
    ],
    "workers": [
    {
    "name": "guestbook",
    "script": "guestbook-guestbook",
    "compatibilityDate": "2026-10-01",
    "handlers": [
    "fetch"
    ],
    …
    "resources": [
    {
    "kind": "d1",
    "name": "book",
    "binding": "BOOK",
    "physicalName": "guestbook-book",
    …
  • infra plan --json: the plan, with check’s findings in it, each resource’s and Worker’s status, ship’s plan for each Worker, and contract changes.

You don’t have to remember to run them:

  • In your editor, the type errors appear as you type, since they’re the compiler’s.
  • infra plan runs the offline checks (all but the bundles, which the plan makes itself) and prints what they find above the plan.
  • infra ship runs them too, and stops on an error before it touches your account. --no-check skips them.

Checks only ever say what’s wrong. Your declarations are the source of truth, and a check never edits them.

  • declared-unused is a warning, not an error: binding something you don’t use costs nothing at runtime, but it’s often a sign of a leftover. Files (Assets.Directory) are exempt, since Cloudflare serves them without your code.
  • The type check covers the whole project the app is in, so an error in a test file shows up too.
  • There’s no editor plugin for the non-type checks yet: run infra check.