← Workers Clinic / API
Tokens

Drive Workers Clinic from your own code

Everything the web page does is available over HTTP: paste a Cloudflare Workers project in, get the same structured production-readiness review back. The natural use is a CI job that re-reviews the Worker whenever wrangler.jsonc or anything under src/ changes, and fails the build when readiness comes back not-ready or a finding arrives at priority: "critical". The same call run across a fleet of Workers gives you twelve identical checks per service, diffable row by row.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same envelope, so one helper covers the whole API:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

Send your app slug as X-App-Slug: workers-clinic and your token as Authorization: Bearer … on every call.

Error codes

codestatuswhat to do
unauthorized401The token is missing, malformed or expired. Get a new one from the token page.
payment_required402The balance is below min_credits. Call /estimate first, compare hold_credits against the balance from /me, and top up before submitting.
forbidden403The token is valid but not for this app, or a guest token tried a metered run. Check the X-App-Slug header, and sign in for a personal token.
not_found404Unknown job id, unknown collection, or the app slug does not exist. A reviews collection that is not declared on the release you are calling also lands here.
conflict409The same Idempotency-Key was replayed with a different body. Bump the attempt suffix, or send the original input back unchanged.
validation_error422The input object is missing a required field — files is the usual one — or a field is the wrong type. prescan_facts must be an object with resources and flags arrays, not a bare array.
rate_limited429Too many requests. Back off and retry; do not tight-loop the job poller — two seconds between polls is plenty.
internal5xxA server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice.

1. A tiny client

One helper that adds the headers, unwraps data and raises on error.

# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="workers-clinic"
TOKEN="YOUR_TOKEN"        # from https://workers-clinic.skillsafe.ai/tokens.html

call() {                  # call <path> [json-body]
  if [ -n "$2" ]; then
    curl -sS -X POST "$BASE/$1" \
      -H "Authorization: Bearer $TOKEN" \
      -H "X-App-Slug: $SLUG" \
      -H "Content-Type: application/json" \
      -d "$2"
  else
    curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG"
  fi
}

2. Get a token

The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. You never need to open the developer console.

A guest token can call /me and /estimate. Reviewing a Worker is metered, so /run and /run-stream need a personal token from signing in — a guest token that posts a run gets a 403 forbidden. Each POST /guest also mints a new guest subject, which matters later: the reviews collection is scoped to the calling subject, so a fresh guest token sees an empty history.

# A guest token is enough for /me and /estimate. Running a review is metered and
# needs a personal token: open the token page and press "Sign in".
#
#   https://workers-clinic.skillsafe.ai/tokens.html
#
# That page also gives you a ready-made shell export:
#   export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead:
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" -H "X-App-Slug: workers-clinic"

3. Check the session and the balance

GET /me tells you whether the token is a guest or a person, and what the balance is. subject_type is user for a personal token and guest for a guest one — branch on it before you spend a call finding out the hard way. Compare credits against hold_credits from the next step before you run, so a shortfall surfaces as your own clear message rather than a 402. The web app does exactly this: if the balance is under the reservation it says so and offers a top-up link instead of submitting.

call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
#
# subject_type is "guest" for a guest token; a guest can /estimate but not /run.

4. Price the run — free

The input object is exactly what the app's own form submits:

fieldtypemeaning
filesstring, requiredThe pasted project — wrangler.jsonc or wrangler.toml, the Worker entrypoint, Durable Object and WorkerEntrypoint classes, generated or hand-written types, sometimes package.json. Put a // file: src/index.ts marker line above each file so they can be told apart; a single file with no marker is accepted. This is the review's only evidence. If you clipped the middle of a file, say so with a // [... clipped N characters ...] marker — the review will not infer anything about what was cut.
targetstringThe deploy target to review against: production, staging, preview or unknown. It changes what is acceptable, not what is true — a stale compatibility_date on a preview Worker is a note, on a production Worker fronting customer traffic it is a finding.
focusstringgeneral, config, bindings, runtime, types, security or observability. It reorders the findings and shapes focus_areas; it never lets a whole category be skipped, and a high-severity finding from elsewhere is never suppressed.
contextstring, optionalFree text about traffic, exposure, history and deadlines — "public API behind a Cloudflare route, 2k req/s at peak, first review before it takes real traffic". Used for prioritisation. It is treated as a claim, not as evidence.
prescan_factsobject, optional but strongly recommended{resources: [{id,label}], flags: [{id,label}]} — see below.
retry_notestring, optionalSend only on a retry, when a previous reply failed to parse or came back truncated. The instruction is obeyed exactly. This is the lane the web page uses for its one automatic reformat retry.

What prescan_facts is for

Before it sends anything, the browser app runs a deterministic client-side parse of the paste: it reads wrangler.jsonc as real JSONC and wrangler.toml as real TOML, pulls out the worker name, entrypoint, compatibility_date, compatibility flags, named environments and every declared binding by type, then walks the source for a fixed list of anti-patterns. What it establishes goes into resources; what it objects to goes into flags, each with a stable id.

Those facts are then handed to the model as things it must reconcile. Every flags id has to come back exactly once in coverage_check — either addressed: true with a finding covering it, or addressed: false with a one-sentence note saying why the parser was wrong in context. resources are treated as established fact and are not contradicted: if the prescan says compatibility_date 2023-05-18, that is the date.

This is the single biggest quality difference between the web app and a naive API call. A caller who omits prescan_facts gets a review with an empty coverage_check — nothing to hold the model to, and no way to tell a fact it weighed from a fact it never saw. A caller who supplies it gets every flag either confirmed with a finding id or explicitly set aside with a reason. Ids are yours to choose, but the app's own shapes are worth copying, because they encode where the problem is:

config:compat-date-stale                     compatibility_date is more than a year behind
config:compat-date-missing                   no compatibility_date at all
config:node-import:node:crypto               a Node built-in imported without nodejs_compat
config:var-secret:SESSION_SECRET             something credential-shaped in plain `vars`
bindings:undeclared:RATE_LIMIT_KV            env.X is read but nothing declares it
bindings:unused:UPLOADS                      declared but never read
bindings:rest-api:src/index.ts:34            the Cloudflare REST API called from inside a Worker
bindings:hyperdrive-missing                  a direct Postgres/MySQL connection, no Hyperdrive
runtime:global-state:responseCache           a module-level mutable holding request data
runtime:unbounded-body:src/index.ts:27       await upstream.text() with no size bound
runtime:floating-promise:src/index.ts:31     a promise neither awaited nor handed to waitUntil
runtime:ctx-destructured:src/index.ts:14     `const { waitUntil } = ctx`
runtime:passthrough-on-exception:src/index.ts:15
security:math-random:src/index.ts:21         Math.random() used to build an identifier
security:hardcoded-secret:UPSTREAM_TOKEN     a credential literal in source
security:timing-unsafe-compare:SESSION_SECRET
types:any:src/index.ts:23                    an `any` annotation
types:hand-written-env                       Env declared by hand instead of `wrangler types`
observability:disabled                       observability.enabled is not true
observability:no-sampling                    enabled with no explicit head_sampling_rate

label is one human sentence describing the fact; it is what the model reads, so make it concrete ("Module-level Map responseCache caches response bodies across requests") rather than generic ("global state").

/estimate

POST /estimate creates no job and charges nothing. It returns the model binding — model, model_alias, markup_bps — and the reservation: hold_credits is what gets held, min_credits is the balance you must clear to start, and sponsor_enabled says whether the app is covering the run. The hold is priced at the full output cap, so the charged_credits that comes back on the finished job is normally far lower — a review that reserves a couple of thousand credits typically settles for a few hundred. Comparing hold_credits against the credits from GET /me before you submit is how you turn a 402 into a message of your own.

INPUT='{"files": "// file: wrangler.jsonc\n{\n  \"name\": \"edge-api\",\n  \"main\": \"src/index.ts\",\n  \"compatibility_date\": \"2023-05-18\",\n  \"kv_namespaces\": [{ \"binding\": \"CACHE\", \"id\": \"9d1f0e7c4b2a4f9e\" }],\n  \"vars\": { \"UPSTREAM_BASE\": \"https://origin.internal.example.com\" }\n}\n\n// file: src/index.ts\ninterface Env {\n  CACHE: KVNamespace;\n  UPSTREAM_BASE: string;\n}\n\nconst responseCache = new Map();\n\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext) {\n    const traceId = \"req-\" + Math.random().toString(36).slice(2);\n    const url = new URL(request.url);\n    if (responseCache.has(url.pathname)) return new Response(responseCache.get(url.pathname));\n    const upstream = await fetch(env.UPSTREAM_BASE + url.pathname);\n    const body = await upstream.text();\n    responseCache.set(url.pathname, body);\n    return new Response(body);\n  }\n};", "target": "production", "focus": "general", "context": "Public API behind a Cloudflare route, ~2k req/s at peak. First review before it takes real traffic.", "prescan_facts": {"resources": [{"id": "config:file:wrangler.jsonc", "label": "wrangler.jsonc read as JSONC"}, {"id": "config:compat:2023-05-18", "label": "compatibility_date 2023-05-18"}, {"id": "config:binding:kv:CACHE", "label": "KV namespace binding CACHE"}], "flags": [{"id": "config:compat-date-stale", "label": "compatibility_date 2023-05-18 is more than a year behind"}, {"id": "runtime:global-state:responseCache", "label": "Module-level Map responseCache caches response bodies across requests"}, {"id": "security:math-random:src/index.ts:21", "label": "Math.random() builds the trace identifier"}]}}'

call estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
#   "markup_bps":1000,"hold_credits":2140,"min_credits":310,"sponsor_enabled":false}}
#
# estimate is FREE. It creates no job and charges nothing. hold_credits is what
# gets RESERVED at the full output cap; the charge afterwards is normally much lower.

5. Run it, then poll

POST /run takes {"input": {…}} and returns a job_id; poll GET jobs/{job_id} until status is succeeded or failed. The review JSON is the string at data.output.output.

Always send an Idempotency-Key. Derive it from the input, as the web app does — it uses workers-clinic:<hash>:<length>:a<attempt>, e.g. workers-clinic:1a2b3c4d:1812:a1. A retried request carrying the same key returns the same job instead of billing a second run, which is exactly what makes a CI retry after a dropped connection safe: a retry with a fresh key double-bills. Replaying a key with a different body is a 409 conflict, so bump the attempt suffix whenever the input actually changed — which is also how the app distinguishes its automatic reformat retry (:a2) from a deliberate re-run.

# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
KEY="workers-clinic:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"

JOB=$(curl -sS -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-App-Slug: $SLUG" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d "{\"input\": $INPUT}" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

# Poll until the job reaches a terminal status. Two seconds is plenty - a tight
# loop earns a 429.
while :; do
  OUT=$(call "jobs/$JOB")
  STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
  [ "$STATUS" = "succeeded" ] && break
  [ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
  sleep 2
done
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])'

6. Or stream it

POST /run-stream is the same call over server-sent events: an event: job with the job id, a run of event: delta frames each carrying a chunk of the JSON review in {"text": "…"}, and a terminal event: done carrying status, charged_credits and the truncated flag. Send the same Idempotency-Key you would send to /run.

The app's own page uses this endpoint, and the trick it uses is worth stealing: because the model emits the object's keys in a fixed order, you can drive a staged progress list purely by watching for the top-level JSON keys as they arrive in the accumulated text. No parsing, no partial JSON repair — a substring search for "checks" is enough to mark the checks stage done and light up the next one. The stages the page advances through are:

Read the project                                            (no key - it starts immediately)
Establish the Worker, its entrypoint and its readiness      "worker_name"   "readiness"
Map every binding against every env read                    "bindings"
Work the twelve checks                                      "checks"
Write the findings with corrected fragments                 "findings"
Reconcile the prescan                                       "coverage_check"
Produce the hardened config and the verification commands   "hardened_config"  "commands"
Choose focus areas and summarize                            "focus_areas"   "summary"

The other reason to stream is recovery. If the connection dies mid-flight you still hold every byte that arrived, and the sections that completed are readable — the app keeps whatever parsed rather than discarding the run. A truncated: true on the done event means the reply hit the output cap: what you hold is a prefix, not a review, and the fix is a retry with a retry_note asking for fewer, denser findings, not an attempt to repair the JSON.

# Server-sent events. Each `delta` carries a chunk of the JSON review; the final
# `done` event carries the status, charged_credits and the truncated flag.
curl -N -X POST "$BASE/run-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-App-Slug: $SLUG" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -H "Accept: text/event-stream" \
  -d "{\"input\": $INPUT}"

# event: job    {"job_id":"job_..."}
# event: delta  {"text":"{\"review_name\":\"edge-api"}
# event: delta  {"text":" - production readiness review\",\"readiness\":\"not-ready\","}
# event: delta  {"text":"\"checks\":[{\"check\":\"compatibility_date\","}
# event: done   {"status":"succeeded","charged_credits":517,"truncated":false}

7. Parse the review and check the reconciliation

data.output.output is a string holding one JSON object, so unwrap it twice: the envelope, then the review. Be tolerant in the same way the app is — strip a stray code fence, then slice from the first { to the last } before parsing.

Three invariants are worth enforcing on your side, because the app enforces them too:

  1. Every prescan_facts.flags id appears exactly once in coverage_check, and no id appears that you did not send. A flag missing from the reconciliation means a fact you established was quietly skipped — treat that as a failed run, not a passing one, and retry with a retry_note naming the missing ids.
  2. checks has all twelve entries, in the documented order. Fewer means the reply was cut short or the model improvised; the app renders the table by index, so a short array is a defect.
  3. Every focus_areas[].finding_ids entry names a real findings[].id. The app silently drops unknown ids rather than showing a dangling reference, which is convenient in a UI and misleading in a script — check it explicitly.

It is also worth applying the same coercions the app applies, so your consumer never sees a value outside the documented enums: an unrecognised category becomes config, an unrecognised severity, likelihood or priority becomes medium, an unrecognised readiness becomes needs-work, an unrecognised check status becomes unknown, and a missing worker_name, entrypoint, compat_date or deploy_target becomes the string "unknown". Findings with neither a problem nor a fix are dropped; if that empties the array, the reply is not a review — the app treats it as a parse failure and retries once with a retry_note.

# The review JSON is a string inside the envelope, so unwrap it twice.
REVIEW=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])')

printf '%s' "$REVIEW" | python3 -c '
import sys, json
r = json.load(sys.stdin)
print(r["readiness"], "|", r["verdict"])
print(r["worker_name"], r["entrypoint"], r["compat_date"], r["deploy_target"])
for c in r["checks"]:
    print(f"  {c[\"status\"]:8} {c[\"check\"]:28} {c[\"evidence\"]}")
for b in r["bindings"]:
    print(f"  {b[\"binding\"]:16} {b[\"type\"]:18} declared={b[\"declared\"]} used={b[\"used\"]}")
for f in r["findings"]:
    print(f"  {f[\"id\"]} {f[\"priority\"]:8} {f[\"category\"]:14} {f[\"resource\"]}")
print(r["hardened_config"])
print("\n".join(r["commands"]))
'

# Every prescan flag id must come back exactly once in coverage_check.
printf '%s' "$REVIEW" | python3 -c '
import sys, json
seen = [c["id"] for c in json.load(sys.stdin)["coverage_check"]]
want = ["config:compat-date-stale", "runtime:global-state:responseCache",
        "security:math-random:src/index.ts:21"]
missing = [i for i in want if seen.count(i) != 1]
if missing:
    raise SystemExit("unreconciled prescan flags: " + ", ".join(missing))
print("coverage_check reconciles")
'

The output contract

data.output.output is a JSON string holding one object. This is exactly what the web app parses, so anything that renders here will render there. Trimmed to two checks, two findings and short strings:

{
  "review_name": "edge-api — production readiness review",
  "readiness":   "not-ready",
  "verdict":     "A module-level Map caches response bodies across requests, so one user's response can be served to another.",
  "worker_name": "edge-api",
  "entrypoint":  "src/index.ts",
  "compat_date": "2023-05-18",
  "deploy_target": "production, internet-facing",
  "exec_summary": "edge-api is a thin read-through cache in front of an internal origin...\n\nThe shape of the problem is request-scoped state...\n\nDo the cache first; the rest is config work.",
  "assumptions": [
    "The Worker is deployed with `wrangler deploy` from this config; no named environment was pasted."
  ],
  "open_questions": [
    "Is the upstream response ever user-specific? If so the module-level cache is a data-leak, not just a correctness bug."
  ],
  "bindings": [
    { "binding": "CACHE", "type": "KV namespace", "declared": true, "used": false,
      "note": "declared in wrangler.jsonc but nothing in src/index.ts reads env.CACHE" },
    { "binding": "UPSTREAM_BASE", "type": "plain var", "declared": true, "used": true,
      "note": "read once in fetch to build the upstream URL" }
  ],
  "checks": [
    { "check": "compatibility_date", "status": "fail",
      "evidence": "wrangler.jsonc sets \"compatibility_date\": \"2023-05-18\"",
      "requirement": "A compatibility_date within the last few months, bumped deliberately." },
    { "check": "Request-scoped state", "status": "fail",
      "evidence": "src/index.ts:16 `const responseCache = new Map()` at module scope, written per request",
      "requirement": "No module-level mutable holding request data; use the KV binding or the Cache API." }
  ],
  "findings": [
    {
      "id": "WC-001",
      "category":   "runtime",
      "severity":   "high",
      "likelihood": "high",
      "priority":   "critical",
      "resource": "src/index.ts:16",
      "problem":  "`responseCache` is a module-level Map written on every request (runtime:global-state:responseCache).",
      "impact":   "Module scope is shared by every request an isolate serves, so a body cached for one path or one user is returned to the next caller of that path.",
      "fix":      "Delete the Map and read through the CACHE KV binding, or use the runtime Cache API keyed on the request.",
      "snippet":  "const hit = await env.CACHE.get(url.pathname);\nif (hit) return new Response(hit);"
    },
    {
      "id": "WC-002",
      "category":   "config",
      "severity":   "medium",
      "likelihood": "high",
      "priority":   "high",
      "resource": "wrangler.jsonc",
      "problem":  "compatibility_date is 2023-05-18 (config:compat-date-stale).",
      "impact":   "The Worker runs on a runtime two years behind the one you test against locally.",
      "fix":      "Bump compatibility_date to today's date and re-run the test suite before deploying.",
      "snippet":  "\"compatibility_date\": \"2026-08-12\""
    }
  ],
  "coverage_check": [
    { "id": "config:compat-date-stale", "addressed": true, "note": "WC-002." },
    { "id": "runtime:global-state:responseCache", "addressed": true, "note": "WC-001." },
    { "id": "security:math-random:src/index.ts:21", "addressed": false,
      "note": "The value is only a log trace id, never a token or a cache key, so Math.random is adequate here." }
  ],
  "hardened_config": "{\n  // Bumped: the old date pinned a two-year-old runtime.\n  \"compatibility_date\": \"2026-08-12\",\n  \"name\": \"edge-api\",\n  \"main\": \"src/index.ts\",\n  \"observability\": { \"enabled\": true, \"head_sampling_rate\": 0.1 },\n  \"kv_namespaces\": [{ \"binding\": \"CACHE\", \"id\": \"9d1f0e7c4b2a4f9e\" }],\n  \"vars\": { \"UPSTREAM_BASE\": \"https://origin.internal.example.com\" }\n  // Secrets: npx wrangler secret put UPSTREAM_TOKEN\n}",
  "commands": [
    "npx wrangler types                 # regenerate Env from the config",
    "npx tsc --noEmit                   # the generated Env must typecheck",
    "npx wrangler deploy --dry-run      # config validation without deploying"
  ],
  "quick_wins": [
    "Add \"observability\": { \"enabled\": true, \"head_sampling_rate\": 0.1 } to wrangler.jsonc."
  ],
  "focus_areas": [
    { "area": "Request-scoped state", "why": "The one issue that can serve one caller's data to another.",
      "finding_ids": ["WC-001"] },
    { "area": "Config freshness", "why": "The runtime you deploy onto is not the one you test against.",
      "finding_ids": ["WC-002"] }
  ],
  "summary": "One paragraph you can paste into the pull request."
}

Every key

keytypemeaning
review_namestringShort title naming the Worker and the target reviewed against. Defaults to "Untitled Workers review" if the model omits it.
readinessenumready, needs-work or not-ready. The single value a CI gate should branch on.
verdictstringOne sentence naming the single thing that decides the readiness.
worker_namestringThe name from the config, or "unknown" if no config was pasted. Never guessed.
entrypointstringThe main from the config, e.g. "src/index.ts", or "unknown".
compat_datestringThe compatibility_date the config shows, or "unknown". This is the paste's value, not a recommendation — the recommendation lives in hardened_config.
deploy_targetstringThe target input echoed back in words, e.g. "production, internet-facing". Free text, not an enum — do not branch on it; branch on the target you sent.
exec_summarystringTwo or three short paragraphs separated by blank lines: what the Worker does, what shape it is in, what to do first. Plain text — Markdown inside it renders literally.
assumptionsstring[]What had to be assumed because it was not pasted — which environment deploys, whether a secret was pushed, what a clipped region contained.
open_questionsstring[]Questions whose answers would change the review or its ordering.
bindingsobject[]{binding, type, declared, used, note}, one row per binding the config declares or the code reads. declared and used are booleans; note is one clause. The interesting rows are the asymmetric ones: declared: false, used: true is an env.X read with nothing behind it, declared: true, used: false is config carrying weight for nothing.
checksobject[]{check, status, evidence, requirement}. Always the same twelve check strings in the same fixed order — render by index, do not search by name. evidence quotes or names the exact line, file and construct; requirement is what would make it pass, in one sentence.
findingsobject[]{id, category, severity, likelihood, priority, resource, problem, impact, fix, snippet}. Ids are sequential WC-001, WC-002, … Always at least one entry — a genuinely clean Worker gets the highest-value remaining improvement at low priority and readiness: "ready". One finding per distinct problem, not one per occurrence. resource is path/to/file.ts:LINE where possible, otherwise the config key. snippet is a pasteable corrected TypeScript or JSONC fragment, or "" rather than a guess.
coverage_checkobject[]{id, addressed, note}. Exactly one entry per prescan_facts.flags id, and no ids the prescan did not send. addressed: true means a finding covers it and note carries the finding id; addressed: false means it was deliberately set aside, with the one-sentence reason in note. Empty if you sent no prescan_facts.
hardened_configstringA complete, valid wrangler.jsonc for this Worker with the config-level findings applied: a current compatibility_date, the compatibility flags it needs, observability with an explicit sampling rate, and every binding the code actually reads. JSONC comments are part of the value and are what makes it readable. No secret values — a secret appears as a comment naming the wrangler secret put command that sets it. Trimmed; "" if no config could be produced.
commandsstring[]Ordered shell commands that verify the result, each with a trailing comment. Read-only, and the last one is a real deploy dry run — typically npx wrangler types, npx tsc --noEmit, npx wrangler deploy --dry-run.
quick_winsstring[]Changes worth under fifteen minutes each, phrased as instructions.
focus_areasobject[]{area, why, finding_ids}, two to four themes. Every id in finding_ids must exist in findings — the app filters unknown ids out silently, so check them yourself.
summarystringOne paragraph, written to be pasted into a pull request.

The enums

fieldvaluesnotes
readinessready, needs-work, not-readynot-ready means at least one finding would leak data, drop writes or break at runtime under real traffic. needs-work means named problems remain but nothing fails today. ready means the Worker can take traffic and what is left is improvement work. An unrecognised value coerces to needs-work.
findings[].categoryconfig, bindings, runtime, types, security, observability, architectureconfig is the wrangler file, bindings is the declaration/use join and anything reaching a service the wrong way, runtime is behaviour under load — streaming, promises, module-level state — and architecture is the shape of the Worker itself. An unrecognised value coerces to config.
findings[].severity
findings[].likelihood
low, medium, highSeverity is how bad it is if it happens; likelihood is how reachable it is from the code as pasted. Both coerce to medium if unrecognised.
findings[].prioritycritical, high, medium, lowSeverity by likelihood, adjusted for target: a leaked credential on a production Worker is critical; the same pattern in a preview Worker with a test key is medium. This is the field a CI gate should count. Coerces to medium.
checks[].statuspass, fail, weak, unknownunknown is a legitimate answer — and the required one — when the paste does not contain the evidence: a check whose evidence was never pasted is not a passing check. weak means the thing exists but does not do its job, such as observability.enabled with no head_sampling_rate. Coerces to unknown.
bindings[].declared
bindings[].used
true, falseBooleans, coerced from whatever arrives. A secret read from env that is absent from the config is not a missing binding — it is the correct pattern, and the note asks you to confirm wrangler secret put was run.

The twelve checks

checks always carries these twelve check strings, in this order, on every run — so a table can be rendered by index and two reviews of the same Worker are diffable row by row:

 1.  compatibility_date            present, and recent enough for the target
 2.  nodejs_compat                 set when a Node built-in is imported, absent when nothing needs it
 3.  Generated types               Env from `wrangler types`, no `any`, no `as unknown as` double casts
 4.  Secrets                       nothing credential-shaped in source or plain vars; compared with
                                   crypto.subtle.timingSafeEqual, never ===
 5.  Bindings declared and used    every env.X read is declared, every declaration is read, and
                                   Durable Object classes use this.env
 6.  Response streaming            no unbounded .text() / .json() / .arrayBuffer() - 128 MB, no swap
 7.  Promise handling              every promise awaited, returned, voided or given to ctx.waitUntil;
                                   ctx never destructured
 8.  Bindings over REST            in-process and service bindings instead of the Cloudflare REST API
 9.  Hyperdrive                    external PostgreSQL or MySQL goes through a Hyperdrive binding
10.  Observability                 observability.enabled true with an explicit head_sampling_rate,
                                   and one structured JSON object per log event
11.  Request-scoped state          no module-level mutables or caches holding request data
12.  Error handling                explicit try/catch with structured error responses, never
                                   ctx.passThroughOnException()

The review never echoes a secret value. If the paste contains a hardcoded token, an API key or a credential in plain vars, the finding names the identifier and says to move it to wrangler secret put and rotate it — the value itself does not appear in problem, snippet or hardened_config.

Your saved reviews

Every run the app completes is written to the reviews collection, so a review follows the user across devices. It is declared acl_read: owner and acl_write: user: rows are scoped to the calling subject, which means a script must reuse one token across the run and the query or it will see an empty collection. Each POST /guest mints a new guest subject, so guest tokens are not a way to share history.

fieldtypemeaning
uidstringThe app's own id for the run — stable across a re-render, and the key the local mirror and the account rows are merged on.
titlestringreview_name from the reply.
readinessstringready, needs-work or not-ready.
verdictstringThe one-sentence verdict.
worker_namestringThe Worker the review was about, or unknown.
compat_datestringThe compatibility_date the paste showed. Handy for "which of our Workers are still on a 2023 runtime?".
input_hashstringHash of the submitted input — the cheap way to tell whether a Worker actually changed between runs before spending credits on a re-review.
findings_countnumberfindings.length.
critical_countnumberHow many findings came back priority: "critical" or "high".
ran_attimestampISO-8601, set when the run completed. The natural sort key.

Those ten fields are declared, and therefore filterable and sortable. The rest of the document — the whole review, its meta and the input it ran against — round-trips intact under entry but is not indexed. Documents are capped at 64 KB, and the app drops entry.input before it drops the review itself, so a very large paste may not come back with its row.

embed is ["title", "worker_name", "verdict", "readiness"], so POST /collections/reviews/similar with a text or a record_id finds past reviews that read like this one — useful for "have we seen this failure shape before?" across a fleet of Workers.

Two shapes catch people out. Sorting takes sort, an object ({"field": "ran_at", "dir": "desc"}); an order_by array is silently ignored, and you get insertion order with no error. And every where entry must be an operator object — eq, ne, lt, lte, gt, gte, in (up to 20 values) or contains. The bare-value shorthand {"readiness": "not-ready"} is rejected. Records come back wrapped: data.records[].doc holds the fields, alongside a record_id — never read the fields flat off the record.

# Your saved reviews, newest first. Note `sort` is an object, not an array.
call collections/reviews/query '{"sort":{"field":"ran_at","dir":"desc"},"limit":10}'

# Only the ones that came back not-ready with something critical in them.
call collections/reviews/query '{"where":{"readiness":{"eq":"not-ready"},"critical_count":{"gte":1}},
  "sort":{"field":"ran_at","dir":"desc"},"limit":20}'

# Which Workers are still pinned to an old runtime?
call collections/reviews/query '{"where":{"compat_date":{"contains":"2023"}},"limit":50}'

# Past reviews that read like this one, over embed = title, worker_name, verdict, readiness.
call collections/reviews/similar '{"text":"module-level cache shared across requests","limit":5}'

A CI recipe

This is the natural use: a job that re-reviews the Worker whenever wrangler.jsonc or anything under src/** changes, and fails the build when readiness comes back not-ready or any finding arrives at priority: "critical". Everything else — needs-work, a handful of high findings — is a warning, because a gate that fires on every review is a gate people turn off.

Four things make it cheap and safe to run on every push. Concatenate the tracked files into files with // file: markers, so the review sees the same paths your reviewers do. Derive the Idempotency-Key from that string, so a re-run of the same commit — a flaky runner, a manual re-trigger — replays the original job instead of billing again. Send prescan_facts if you have any deterministic linting of your own, so the model has to reconcile it. And call /estimate first: it is free, and comparing hold_credits against the /me balance turns a mid-pipeline 402 into a clear message before anything is reserved.

# .github/workflows/workers-clinic.yml
name: Workers Clinic
on:
  pull_request:
    paths:
      - "wrangler.jsonc"
      - "wrangler.toml"
      - "src/**"

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Review the Worker
        env:
          SKILLSAFE_TOKEN: ${{ secrets.SKILLSAFE_TOKEN }}
        run: python3 .github/scripts/workers_clinic.py
#!/bin/sh
# Fail the build on not-ready or on any critical finding.
set -eu
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="workers-clinic"
TOKEN="$SKILLSAFE_TOKEN"          # a repository secret; see /tokens.html

# 1. Build `files` from the tracked config and sources, with // file: markers.
FILES=$(for f in wrangler.jsonc wrangler.toml $(git ls-files 'src/*.ts'); do
  [ -f "$f" ] || continue
  printf '// file: %s\n' "$f"
  cat "$f"
  printf '\n\n'
done)

# 2. One JSON body, and an Idempotency-Key derived from it: a re-run of the same
#    commit replays the original job instead of billing a second review.
INPUT=$(FILES="$FILES" python3 -c '
import json, os
print(json.dumps({"input": {
  "files": os.environ["FILES"],
  "target": "production",
  "focus": "general",
  "context": "CI gate on every pull request that touches wrangler config or src/.",
  "prescan_facts": {"resources": [], "flags": []}
}}))')
KEY="workers-clinic:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"

# 3. Price it first - /estimate is free and creates no job.
printf '%s' "$INPUT" > /tmp/wc-input.json
curl -sS -X POST "$BASE/estimate" \
  -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG" \
  -H "Content-Type: application/json" -d @/tmp/wc-input.json

# 4. Run, poll, gate.
JOB=$(curl -sS -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" \
  -d @/tmp/wc-input.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

while :; do
  OUT=$(curl -sS "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG")
  STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
  [ "$STATUS" = "succeeded" ] && break
  [ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
  sleep 2
done

printf '%s' "$OUT" | python3 -c '
import sys, json
r = json.loads(json.load(sys.stdin)["data"]["output"]["output"])
crit = [f for f in r["findings"] if f["priority"] == "critical"]
for f in r["findings"]:
    lvl = "error" if f["priority"] == "critical" else "warning"
    print(f"::{lvl} file={f[\"resource\"].split(\":\")[0]}::{f[\"id\"]} {f[\"problem\"]} -- {f[\"fix\"]}")
print(r["summary"])
if r["readiness"] == "not-ready" or crit:
    raise SystemExit(f"::error::Workers Clinic: {r[\"readiness\"]} ({len(crit)} critical)")
'

One last note on cost control: before you spend a run, query the reviews collection for the newest row and compare its input_hash with the hash of what you are about to send. If they match, nothing about the Worker changed since the last review and the credits are better spent somewhere else — the app uses the same hash for exactly this purpose.