EveryAppEveryApp

Workers AI for the Gateway

Step-by-step guide: a Workers AI token (WORKERS_AI_API_TOKEN) on the Gateway for apps that list workers-ai: the token, the account, the check, rotation.

Workers AI runs Meta's Llama and other open models inside the Gateway's own Cloudflare account, behind the Gateway's AI proxy. openmail lists it in an optional feature's providers beside a required openrouter, so secret list shows the token in settings with set: false, never in unmet: the inbox opens without it. Its Worker calls Workers AI (the Email Agent, the draft verifier and the prompt-injection scanner, on /api/ai/workers-ai/v1/chat/completions) only when built with AI_PROVIDER=workers-ai, which is still the build default until the inbox's PR #6 flips it to OpenRouter; a Worker built with it and no token says so in the chat. Only the token is missing: the account is CLOUDFLARE_ACCOUNT_ID, which everyapp setup set. At the end the person has one Cloudflare API token limited to Workers AI on that account, held by the Gateway, and a Workers AI model answering through it.

Settings

Name Required What it holds
WORKERS_AI_API_TOKEN optional for openmail (in an optional feature's providers: ["workers-ai"]); needed only by a Worker built with AI_PROVIDER=workers-ai A Cloudflare API token with Workers AI Read on the Gateway's account (and Edit when made by hand: Cloudflare's REST guide asks for both on a custom token): the token string alone, one line, no Bearer. New user tokens start cfut_, account tokens cfat_.
CLOUDFLARE_ACCOUNT_ID set once by everyapp setup; secret set refuses it (SECRET_NOT_SETTABLE from the every-app checkout, SECRET_NOT_DECLARED from an app directory) The account inference runs and bills in: data.accountId in pnpm everyapp status --json, written <account id> below. The token must include this account.

OPENROUTER_API_KEY stays: the inbox's manifest requires it, and OpenSEO and Open CRM use it. This guide adds a provider; it removes none.

Costs and plans

As of October 2026 (Cloudflare's pricing page, updated 2026-10-20):

  • The Gateway's account is on Workers Paid already ($5 a month; everyapp needs it). Workers AI adds no fixed fee, and an API token is free.
  • 10,000 neurons a day are included. Beyond that, Workers Paid bills $0.011 per 1,000 neurons on the monthly invoice; Workers Free stops for the day instead (error 3036).
  • The Workers AI path's default agent model (and its draft verifier), @cf/meta/llama-4-scout-17b-16e-instruct: $0.27 per million input tokens, $0.85 per million output; 300 requests a minute.
  • @cf/moonshotai/kimi-k2.6 (the inbox's optional AGENT_MODEL): paid-only; $0.95 in and $4.00 out per million tokens; 20 requests a minute per account and model on Workers Paid (Cloudflare's 50-a-minute tier is for calls through an AI Gateway on prepaid credits, which the Gateway doesn't use), shared by every organization on the Gateway.

Only a person can

  • Sign in to the Cloudflare dashboard and create the token. cf's OAuth login cannot mint one: cf auth whoami lists no API-token scope, and the token endpoints answer it 403 code 9109. So neither cf accounts tokens create nor the API works from the login; it needs a token to make a token.
  • Copy the token off the screen (shown once) and type or export it in their own terminal.
  • Choose a user token (tied to their login) or an account token (a team's choice; needs API Token Provisioning or Super Administrator on the account), and whether it expires.

Before you start

  1. pnpm everyapp secret list openmail --json, from any directory: the WORKERS_AI_API_TOKEN entry in data.app.setup.settings (names ["WORKERS_AI_API_TOKEN"], need.kind optional, holder gateway, setup setup/workers-ai.md) with "set": false means this guide applies. It is never in unmet (command, pnpm everyapp configure openmail, names it under its feature, with pnpm everyapp provider add workers-ai) and the inbox opens without it; what keeps the inbox closed is OPENROUTER_API_KEY (openrouter.md) and its email sender (email.md). "set": true: ask before replacing (a value can't be read back; replacing means a new token). In the older fields data.gateway.needed has it with "required": false, never in data.gateway.missing. status prints no Guide: line for it (only unmet settings get one): secret list --json, carrying the path as the entry's setup, is how an agent lands here (in the checkout: landing-page/content/agent/setup/workers-ai.md). APP_NOT_FOUND ("openmail isn't deployed to this Gateway yet"): deploy the app first, from its directory (the token can be set before that all the same, from anywhere).
  2. pnpm everyapp status --json: data.installed and data.claimed true; data.accountId is the account the token must include (tell the person; it is not a secret); data.apps[] has "id": "openmail" with "status": "active", "bound": true and the same setup; while a required setting is missing data.fixes has APP_NEEDS_SETUP for it (severity warning, headline "openmail needs setup", run the same command). action_required cloudflare_login with waiting: true (status logs in by itself): show data.url and data.code to the person and keep reading; pnpm everyapp login --json is only for logging in ahead of time.
  3. In 1's output (or pnpm everyapp secret list --json from any directory), data.gateway.names has CLOUDFLARE_ACCOUNT_ID. Missing: stop and tell the person. It is written once, at install, from the account the Gateway lives in (data.accountId is what it should be); secret set refuses it and setup adds nothing to a Gateway that already has a version, so there is no everyapp repair today. The settings model leaves it out (an install-owned setting), so the setup gate doesn't notice it missing: the proxy's 503 (If it fails) is where it shows.
  4. Ask, together with 5 in one message, and wait for both answers before Steps (they change the steps): was the inbox's Worker deployed with AI_PROVIDER=workers-ai (a variable of its own deploy; status doesn't show it)? It decides whether the inbox needs this token at all: the manifest marks it optional, so the inbox opens either way. The build default is still workers-ai (the inbox's PR #6 flips it to OpenRouter; open): a Worker deployed without the variable uses this token; one deployed with AI_PROVIDER=openrouter never does, until its own Worker is deployed again without it, from its repository (its cloudflare.config.ts reads it), not from here: say so, and never redeploy the inbox from this guide.
  5. Ask whether the person already holds a Cloudflare API token with Workers AI on this account (a token from another project is fine if it includes this account). Yes: skip to step 3.

Steps

One step per message, and wait between them: step 1 once the two answers from Before you start are in; step 2, then "copied"; step 3, then "done". The curl line comes in Verify, after that.

  1. You: Say which token to make. A user token (My Profile) is the quick path for a solo operator; an account token keeps working after that person leaves and is the better fit for a team. Both reach Workers AI the same way: the Gateway sends either as a Bearer token to api.cloudflare.com/client/v4/accounts/<account id>/ai/v1/…, and Cloudflare's compatibility matrix lists Workers AI as supporting account tokens (not tried live here). Give them data.accountId to check against the token's account resources.

  2. Person: Create the token. Done when the screen shows the token once, with a copy button and a curl … /user/tokens/verify test under it. Wait for "copied".

    • Prefilled user token: open https://dash.cloudflare.com/?to=/:account/ai/workers-ai (the Workers AI page; pick the Gateway's account if asked), select Use REST API, then Create a Workers AI API Token. A token form opens prefilled (Cloudflare's guide says it grants Workers AI Read on this account, which is what the /ai/* endpoints need; one permission row is expected): check that Account Resources names the Gateway's account (data.accountId), leave TTL (time to live) empty unless they want an expiry (the Gateway stops the day it expires), select Create API Token, then Copy API Token.
    • Custom user token: https://dash.cloudflare.com/profile/api-tokens, Create Token, the custom token at the bottom (label unverified), name it every-app-gateway workers-ai, permissions Account | Workers AI | Read and a second row Account | Workers AI | Edit (Cloudflare's REST guide asks for both on a token made by hand), account resources: Include, the Gateway's account (data.accountId), no client IP filter (the Gateway's egress addresses vary), Continue to summary, Create Token.
    • Account token: in the dashboard pick the Gateway's account, Manage account > Account API tokens, Create Token, the same name and permissions, optional expiration, Continue to summary, Create Token.
  3. Person: Hand it to the Gateway (the setup rules' third way, the hidden read, in their own terminal, in the every-app folder; bash or zsh):

    read -rs WORKERS_AI_API_TOKEN; export WORKERS_AI_API_TOKEN
    pnpm everyapp secret set WORKERS_AI_API_TOKEN --from-env WORKERS_AI_API_TOKEN --json

    After the first line the terminal shows nothing, not even a prompt: paste the token, press Enter, then run the second line. SECRET_EMPTY means the paste didn't land: redo the first line in the same terminal. fish: read -s -x WORKERS_AI_API_TOKEN (it does show read> ), then the same pnpm everyapp secret set … --from-env … line; set -e WORKERS_AI_API_TOKEN clears it afterwards. The last line is "type": "done"; the change is live at once (one new Gateway version, no deploy). pnpm everyapp configure openmail --from-env --json sets it from the same variable (and every other unset setting of the inbox whose variable is exported; its done event's data.set names them). Keep the variable until the smoke check below is done, then unset WORKERS_AI_API_TOKEN. Without the smoke check, the setup rules' first way is simpler: in a second terminal, pnpm everyapp configure openmail (a feature's Gateway setting isn't asked for there: it names pnpm everyapp provider add workers-ai; once the inbox is ready its menu lists WORKERS_AI_API_TOKEN) or pnpm everyapp secret set WORKERS_AI_API_TOKEN for just this one; paste at the hidden prompt. Wait for "done".

  4. You: Run the Verify section. The person runs its curl line in the terminal that still has the variable.

Verify

  1. pnpm everyapp secret list openmail --json, from anywhere: the WORKERS_AI_API_TOKEN entry in data.app.setup.settings has "set": true (it was never in data.app.setup.unmet: optional); data.app.setup.ready doesn't depend on it (OPENROUTER_API_KEY, required by the same manifest, and its email sender do: until then command and data.next name what is left). In the older fields, its data.gateway.needed entry has "set": true, data.gateway.missing never listed it, and data.gateway.names does (for an inbox not deployed yet, the root pnpm everyapp secret list --json shows data.gateway.names and that is the check). Then pnpm everyapp status --json: the openmail entry's setup.ready is true and data.fixes has no APP_NEEDS_SETUP for it, once everything required is set.
  2. Smoke check, in the person's terminal with WORKERS_AI_API_TOKEN exported. The header goes in on stdin (-H @-) so the token never sits in curl's argument list, which the process list shows. The token reaches Workers AI on the Gateway's account (a few neurons):
    printf 'Authorization: Bearer %s\n' "$WORKERS_AI_API_TOKEN" | curl -s -H @- -H "Content-Type: application/json" https://api.cloudflare.com/client/v4/accounts/<account id>/ai/v1/chat/completions -d '{"model":"@cf/meta/llama-4-scout-17b-16e-instruct","messages":[{"role":"user","content":"Say ok."}],"max_tokens":5}'
    expects a JSON body with choices[0].message.content. Any "success":false body is read in If it fails; a 401 starts with the verify curl there. Then unset WORKERS_AI_API_TOKEN.
  3. The app, only when the inbox's Worker was built with AI_PROVIDER=workers-ai (the build default until the inbox's PR #6; Before you start, 4); one built with AI_PROVIDER=openrouter never sends this token and the curl line above is the whole proof. Either way the inbox opens only once every required setting is set (Verify 1): a browser sent to https://<gateway host>/?setup=openmail means something still isn't. Open https://openmail.<app base domain>/, open a mailbox, Show agent panel (the header button), type in "Ask your email agent..." and send. A reply streams in, and the Gateway's ai.proxy.usage line (step 4) says "provider":"workers-ai". A missing token shows up in the panel as "AI isn't set up on this gateway yet, so I can't help with this inbox. An admin sets WORKERS_AI_API_TOKEN with pnpm everyapp secret set WORKERS_AI_API_TOKEN."; any other refusal as the Gateway's message.
  4. The Gateway's logs, when a refusal isn't clear: cf has no terminal tail (cf workers lists none; cf o11y telemetry live-tail only prepares a websocket, not tried here; cf o11y telemetry query is the raw query API). Cloudflare's CLI for it is npx wrangler tail every-app-gateway --format json (after its own wrangler login; wrangler isn't in this checkout). Dashboard, the setup rules' path: https://dash.cloudflare.com/?to=/:account/workers-and-pages, then Workers & Pages → every-app-gateway → Observability (Logs on older accounts) → Live. The proxy writes one JSON line per call: "evt":"ai.proxy.usage" with provider, model, status and usage; "evt":"ai.proxy.upstream_failed" with the message when the request to Cloudflare failed. The Gateway's config turns these logs on; Workers Paid keeps 7 days.

If it fails

A 401 from the chat call: first, in the same terminal, Cloudflare's verify route says whether the string itself is good. A user token (cfut_):

printf 'Authorization: Bearer %s\n' "$WORKERS_AI_API_TOKEN" | curl -s -H @- https://api.cloudflare.com/client/v4/user/tokens/verify

An account token (cfat_): the same header on https://api.cloudflare.com/client/v4/accounts/<account id>/tokens/verify; on the user route it answers 401 code 1000 "Invalid API Token", like any token that route doesn't know. "success":true with "status":"active": the string is good, and the rows for code 10000 and 9109 say what is (the permission or the account). 401 code 1000 on its own route: the string (its row).

Error or symptom Meaning What to do
a browser opening the inbox is sent to https://<gateway host>/?setup=openmail; any other request gets 503 with the body {"error":"app_needs_setup","app":"openmail"} the Gateway's setup gate: a required setting of the inbox is still unset (OPENROUTER_API_KEY or its email sender; this token is optional and never closes the inbox) data.app.setup.command, pnpm everyapp configure openmail, then Verify 1
503 ai_provider_not_configured, "needs WORKERS_AI_API_TOKEN and CLOUDFLARE_ACCOUNT_ID" the Gateway holds no token (the inbox opens anyway: its manifest lists the provider in an optional feature; a Worker built with AI_PROVIDER=workers-ai shows it in the chat as "AI isn't set up on this gateway yet, so I can't help with this inbox. An admin sets WORKERS_AI_API_TOKEN with pnpm everyapp secret set WORKERS_AI_API_TOKEN."), or no account id (which the gate doesn't check) secret list --json; set the token; CLOUDFLARE_ACCOUNT_ID missing: no everyapp repair today, tell the person (Before you start, 3)
provider_not_allowed the app's manifest doesn't list workers-ai under providers openmail lists it; another app adds it to everyapp.config.ts and deploys
model_not_allowed a model id outside the app's ai.models use one from the manifest, or add it and deploy
ai_route_not_allowed a path other than /v1/chat/completions, /v1/embeddings, /v1/responses (the native run route is closed) call the OpenAI-compatible routes
502 ai_upstream_failed the request to api.cloudflare.com failed before an answer retry; the Gateway log line ai.proxy.upstream_failed has the message
401, code 1000 "Invalid API Token" (the verify routes) the string is wrong, expired, rolled or deleted; or a cfat_ token on the user route verify a cfat_ token on /accounts/<account id>/tokens/verify; otherwise make a new token and set it again
401, code 10000 "Authentication error" (the /ai/v1 route: curl's chat completion, or passed through by the Gateway) token invalid, expired, rolled, deleted, IP-filtered, or without Workers AI on this account; or $WORKERS_AI_API_TOKEN empty in that terminal the verify curl above; make a new token with Workers AI Read including data.accountId; set it again
403, code 9109 "Unauthorized to access requested resource" or "Invalid access token" the token's resources don't include this account a token whose account resources include the Gateway's account
403, code 5035 "This model requires a Workers Paid plan." a paid-only model (kimi-k2.6) on an account without Workers Paid the Gateway's account must be on Workers Paid (everyapp needs it anyway)
429, code 3036 "You have used up your daily free allocation of 10,000 neurons." Workers Free's daily cap Workers Paid; it doesn't occur there
429, code 3040 "Capacity temporarily exceeded", or a bare 429 Cloudflare capacity, or a rate limit (kimi: 20 a minute per account; the limit's own code unverified) retry later; on kimi expect it, or use @cf/meta/llama-4-scout-17b-16e-instruct
400 code 5007 "No such model", or 404 code 3042 "The model name is invalid" the model id isn't in the catalog cf ai models list --search llama (read-only) for a current id; change the app's manifest and code
SECRET_EMPTY $WORKERS_AI_API_TOKEN is empty in the terminal that ran secret set redo the read -rs …; export … line in that same terminal, then rerun
SECRET_VALUE_REQUIRED nothing was piped and there is no terminal one of the setup rules' three ways
SECRET_VALUE_REQUIRED from pnpm everyapp configure openmail --json nothing was piped (with --from-env, stdin isn't read), no variable of a missing name was found, and there is no terminal; the message names what the inbox still needs (this token is optional, so never among them) the person's terminal (step 3), NAME=value lines on stdin, or the hidden read then its data.rerun, pnpm everyapp configure openmail --from-env
SECRET_SEVERAL_NAMES --from-env or a pipe with several names one name per call
SECRET_NOT_SETTABLE (from the every-app checkout) or SECRET_NOT_DECLARED (from an app directory) the name is one setup writes (CLOUDFLARE_ACCOUNT_ID) set only WORKERS_AI_API_TOKEN; never add it to the manifest's secrets; a wrong account id has no everyapp repair today (tell the person)
400, code 6003 "Invalid request headers" / 6111 "Invalid format for Authorization header" (the verify routes) $WORKERS_AI_API_TOKEN is empty in that terminal (not exported, or a new terminal); the chat route shows the same as 401 code 10000 redo step 3 in one terminal, in order; an HTML page instead means a mistyped URL

Rerun, rotate, remove

  • Rerun: every step is safe to repeat. configure and secret set replace the value in one new Gateway version; a second token on Cloudflare's side is harmless until you delete it. Interrupted or back later: start at Verify and continue at the first step that isn't done.
  • Rotate: on the token's row in the dashboard (user tokens at https://dash.cloudflare.com/profile/api-tokens, account tokens under Manage account > Account API tokens) the three-dot menu, Roll, Confirm. The new secret keeps the permissions and the old one stops at once, so run step 3 again straight away. The API's roll: cf user tokens roll <token-id> for a user token, cf accounts tokens roll <token-id> for an account token; both need an API token with the matching API Tokens Edit permission, not the login.
  • Remove: pnpm everyapp secret unset WORKERS_AI_API_TOKEN --json (add --yes only when the person said so). The inbox keeps opening (its manifest marks the provider optional); a Worker built with AI_PROVIDER=workers-ai then loses its agent, verifier and scanner to ai_provider_not_configured, one built with AI_PROVIDER=openrouter is unaffected. Then delete the token in the same row menu (label unverified): a token the Gateway no longer holds is still a token.

Sources

Verified 2026-10-23:

On this page