wdl deploy <dir> bundles a Cloudflare Workers-style project with
wrangler deploy --dry-run, then pushes the output to the WDL control plane.
It is not the same as wrangler deploy, which talks directly to Cloudflare.
Do not use wrangler deploy on this platform — only wdl deploy.
Wrangler resolution order is WDL_WRANGLER_BIN, the Worker project's local
wrangler, the CLI package's local wrangler, then PATH. By default there is no
transient npx --yes wrangler fetch; that fallback is allowed only when
WDL_ALLOW_NPX_WRANGLER=1 is set.
WDL hides Wrangler's banner (which skips the normal banner update check) and disables anonymous telemetry for this dry-run subprocess. Wrangler may still consult the configured npm registry when reporting an unknown configuration field. Project build hooks retain their normal network access.
Pick one in this order:
command -v wdlsucceeds — installed globally (npm i -g @wdl-dev/cli). Usewdl ...directly.- Developing inside the wdl-cli repo (
<repo>/bin/wdl.jsexists) — usenode <repo>/bin/wdl.js .... This is the development scenario. - Neither — stop and tell the user; do not invent a path.
In the examples below, treat wdl as a placeholder and substitute the form you
resolved.
The CLI needs three values:
| Value | Purpose |
|---|---|
ADMIN_TOKEN |
Tenant deploy token. Sensitive — never paste it into chat or commit history. |
WDL_NS |
Tenant namespace, e.g. acme, demo-prod. |
CONTROL_URL |
Control-plane URL — from your operator, or your own self-hosted platform (e.g. https://api.wdl.dev). The CLI has no built-in default; it must be configured. |
Recommended path: wdl token set --ns <ns> --control-url <url> reads the
token at a hidden prompt, validates it against /whoami, and stores it 0600 in
~/.config/wdl/credentials — so it never lands in a project file or shell
history. The first stored namespace becomes the default, so later wdl deploy
needs no --ns. One store serves every project on the machine; see
token.md.
Per-repo alternative: when a project should carry its own control URL /
namespace, copy .env.example to .env and fill in the [<ns>] section (the
committed .env.example also documents the shape for teammates). The CLI reads
only ./.env from the directory you run wdl in (there is no upward search), so
run wdl from the directory that holds it. The token stays in the gitignored
.env, never committed.
CI / automation: inject ADMIN_TOKEN, CONTROL_URL, and WDL_NS as
environment variables from your CI secret store — not the interactive token
store, and never a committed .env.
Bare control hosts get a scheme automatically; production hosts default to
https://, local .test / .local or :8080 hosts default to http://. To
force a protocol, write https://... or http://... explicitly.
Precedence: CLI flag > shell env > .env [<ns>] section > .env base section > wdl token store. If none supplies a value, the command fails — there is no built-in
default.
Untrusted projects: wdl deploy runs the project's local Wrangler dry-run
and build hooks as your OS user, so that code can read the on-disk token store
(the credential scrub only keeps WDL variables out of the Wrangler child's
environment, not out of the file). Only deploy projects you trust. For an
untrusted or third-party project, pass an ephemeral --token / --control-url
plus --no-token-store (or WDL_TOKEN_STORE=off) so the CLI ignores the store —
and don't keep a global store at all, since the flag opts out of reading the
file, not its presence on disk. See token.md.
When unsure which value won, run wdl config explain; to confirm which control
the token actually reaches, plus the principal, platform version, and URL hints,
run wdl whoami; for baseline local and remote diagnostics, run wdl doctor.
When the control plane supports /whoami, doctor verifies the remote token,
principal namespace, platform version, and CLI compatibility. Use
wdl doctor --strict in CI when a failed check should make the job fail.
The namespace URL may be (unavailable) when the operator has not configured a
public platform domain; that does not mean authentication failed.
For runtime secrets (distinct from ADMIN_TOKEN), see
secrets.md.
https://<namespace>.<platform-domain>/<worker-name>/<path>
The Worker sees the path with the /<worker-name> prefix stripped. Tenants
have no custom routing capability unless the operator explicitly enables it; do
not add route / routes in a first-time setup.
For local-development control hosts, the deploy summary reuses CONTROL_URL's
scheme and public port when constructing the Worker URL. CONTROL_CONNECT_HOST
changes only the control socket target and never the printed Worker origin.
| Goal | Command |
|---|---|
| Deploy a project | wdl deploy <dir> [--ns <ns>] [--env <name>] [--verbose] |
| List workers | wdl workers |
| Live-tail worker logs | wdl tail <worker> [--raw] |
| Delete a non-live version | wdl delete version <worker> <vN> |
| Delete a worker (preview) | wdl delete worker <worker> --dry-run |
| Inspect Workflow instances | wdl workflows instances <worker> <workflow> |
--ns is optional whenever WDL_NS is set via env or .env, or the wdl token
store has a default namespace. Every subcommand implements --help — run it when
you don't know which flag to use.
- Resolve the CLI invocation form (above).
- Resolve credentials — for a trusted project, prefer
.envor thewdl tokenstore; do not inline environment variables. For an untrusted or third-party project, use an ephemeral--token/--control-urlwith--no-token-storeinstead (see Credentials above — deploy runs project code as you). - Wrangler version check. The bundling step requires
wrangler@^4. If the project pins v3, stop and tell the user — do not silently upgrade. - Install worker dependencies (
npm installin the worker directory) ifnode_modulesis missing. - Pre-create persistent bindings. Read the wrangler config:
[[d1_databases]]→ for eachdatabase_name, check withwdl d1 listfirst; create missing ones withwdl d1 create <name>. See d1.md.[[r2_buckets]]and[[kv_namespaces]]are lazy — no pre-creation needed; the binding works on first use. See r2.md and kv.md.[[queues.*]]— see queues.md; when queue ownership is unclear, confirm with the operator.
- Apply D1 migrations if
migrations_diris set — see d1.md. - Deploy:
wdl deploy .. The CLI prints the upload, the promote, and the runtime URL — show that URL to the user.
The manifest JSON that deploy uploads to control is capped at 32 MiB. Assets are embedded in that JSON request at deploy time; a large static file set can hit the control request cap first. Put bulk or frequently changing files in R2, not in assets.
The control plane enforces a headroomed 1 MiB workerd workerLoader environment
budget (1,040,384 bytes usable). Large [vars], secrets, binding metadata, or
retained versions can fail with worker_env_too_large; reduce the env payload,
or redeploy/delete the retained version named in the error when one is shown.
When the wrangler config has [env.<name>] sections, --env <name> (or
CLOUDFLARE_ENV) is required — the CLI does not pick a default for you. Be
explicit:
wdl deploy . --env preview
wdl deploy . --env productionThe deployed worker name always comes from the top-level name, with no
environment suffix. Wrangler / Cloudflare Workers --env may lead you to expect
names like my-worker-preview; WDL does not append that suffix. See
env-overrides.md for the config shape.
When multiple Wrangler config files exist, the CLI follows Wrangler's priority:
wrangler.json, then wrangler.jsonc, then wrangler.toml.
Both JSON filenames use Wrangler's JSONC syntax, including comments and
trailing commas.
New projects should keep the 2026-06-17 compatibility date unless a feature
requires a newer one. Explicit dates before 2026-04-01, invalid or future
dates, and dates newer than the bundled workerd supports are rejected by
control. Upstream experimental enable flags, legacy_error_serialization, and
allow_irrevocable_stub_storage are unsupported.
Supported: name, main, compatibility_date / compatibility_flags, [vars],
[[kv_namespaces]], [[d1_databases]], [[durable_objects.bindings]],
[[workflows]], [[r2_buckets]], [assets] directory, [triggers] crons,
[[triggers.schedules]] (with timezone, a platform extension),
[[queues.producers]] / [[queues.consumers]], [[services]],
[[platform_bindings]], [[exports]], [env.<name>].
WDL parses [[exports]], [[platform_bindings]],
[[triggers.schedules]], and [[services]].ns itself and removes these private
extensions from the temporary config passed to the Wrangler bundler. Other
fields retain their existing Wrangler passthrough behavior. Wrangler's
object-shaped declarative exports configuration is not supported by WDL.
Tenant JSRPC can serialize Blob values and pass service or Durable Object
class stubs as opaque capability arguments. A receiver may call the delegated
target but cannot rewrite the host-authored caller properties carried by the
stub. Keep delegated stubs in memory; irrevocable persistence is unsupported.
Unsupported (deploy fails): Analytics Engine. Durable Objects supports
same-worker classes only; script_name and rename/delete migrations are not
implemented yet. WDL Workflows supports only workflow classes defined in the
current Worker — not full Cloudflare Workflows parity; script_name,
cross-worker workflows, cross-worker callbacks, service-binding callbacks, and
the Cloudflare source-AST visualizer are not supported. route / routes are
supported only when the operator enables them. Python Workers modules,
unsupported workerd compatibility flags, and WDL-reserved injected module
names are rejected during deploy: the CLI fails fast on local .py modules,
and the control plane is canonical for workerd compatibility and bundle-shape
policy.
Top-level or selected-environment Wrangler runtime/deploy config fields and
sections that WDL would otherwise ignore are also rejected by the CLI, including
legacy [site] Workers Sites, workers_dev, pages_build_output_dir,
observability, limits, placement, and other unsupported binding/config
fields or sections named in the error.
assets.run_worker_first is silently ignored.
Cron triggers and queue consumers are runtime dispatch features; declare them
only on routeable tenant Workers. Workers selected through
[[platform_bindings]] are cold-loaded platform capabilities, not
public/runtime dispatch targets, and cannot declare cron triggers or queue
consumers.
wdl delete worker, wdl delete version, wdl d1 delete, and
wdl secret delete prompt for confirmation by default. If --dry-run exists,
run it first (or do a read-only check), then add --yes only after confirming
with the user. Do not add --yes on your own.
wdl workers reports workflow-defs=yes or workflow-defs=no; unknown
means an older control omitted the field, not that no definitions exist. Worker
delete dry-runs report secret and workflow-definition presence even when a
blocker makes wouldDelete=no.
Deleting a worker does not delete R2 data — see r2.md.
| Symptom | Cause / fix |
|---|---|
wdl: command not found |
The CLI is not on PATH. Inside the wdl-cli repo use node <repo>/bin/wdl.js; otherwise run npm i -g @wdl-dev/cli. |
Missing admin token |
No token resolved. Run wdl token set --ns <ns> --control-url <url> (recommended), or set ADMIN_TOKEN / pass --token / use the [<ns>] section of .env. |
401 unknown_token: unauthorized |
The token is invalid for this control plane / namespace. Re-check ADMIN_TOKEN. |
[vars] must be an object |
Use a [vars] table/object; arrays are invalid. |
[vars] <NAME>: only string/number/boolean values are supported |
Remove nested values; move sensitive strings to a secret. |
binding name collision: <NAME> |
[vars], explicit bindings, or the implicit ASSETS binding reused a runtime env name. Rename one of them. |
experimental_compat_flag_unsupported |
Remove the experimental workerd compatibility flag. |
compatibility_flag_unsupported |
Remove the unsupported compatibility flag named by control. |
python_workers_unsupported |
Python Workers are not supported by WDL; remove Python Worker modules. The CLI also fails fast on local .py modules. |
worker_env_too_large |
Reduce [vars], secrets, or binding metadata; redeploy/delete any retained version named in the error. |
worker_code_too_large |
Reduce generated Worker code size or split the worker. |
worker_code_invalid |
Fix the Worker bundle shape reported by the control plane, including WDL-reserved injected module names. |
wrangler build failed |
Run npx wrangler deploy --dry-run inside the project and fix it there. |
| Deploy succeeds but promote fails | Custom host or service-binding target validation issue; check the binding targets. |
| Worker URL returns 404 | The URL is missing the /<worker-name> segment. |
wdl tail has no history |
Tail is live-only; open wdl tail <worker> before triggering the request. |
tail session_idle / tail session_expired |
Control reclaimed the live-tail stream; the CLI reconnects automatically unless the reconnect cap is reached. |
| Namespace secret did not take effect | NS-level secrets do not force-bump workers; redeploy once or use a worker-level secret. |
| Service binding still hits the old target | Bindings are pinned at caller deploy time; redeploy the caller. |
- ❌ Running
wrangler deployon this platform. It talks to Cloudflare, not WDL. Usewdl deploy. - ❌ Committing a
.envfile containingADMIN_TOKENto git. - ❌ Adding Durable Objects / Workflows config "just in case" — they change the runtime entrypoint and deploy validation; add them only when the code actually uses them.
- ❌ Pinning
wranglerto^3. The bundling step requires v4.
Every example under ../examples/<name> is a deployable project.
../examples/hello-jsonc is the smallest.