Skip to content

Latest commit

 

History

History
217 lines (181 loc) · 29.5 KB

compatibility.md

File metadata and controls

217 lines (181 loc) · 29.5 KB

Workers Compatibility Matrix

This document answers a different question from the module docs: not "where is the implementation?" but "how close is WDL to Cloudflare Workers for a given surface, and what does WDL implement itself?"

Current code and tests remain authoritative. Treat this matrix as the current compatibility contract for WDL's Workers platform shape.

Each row separates four compatibility claims:

  • What workerd provides: the upstream runtime surface WDL reuses.
  • Stronger / added in WDL: platform behavior or guardrails WDL implements beyond the raw workerd surface.
  • Different from Cloudflare: intentional model differences that are not necessarily stronger or weaker.
  • Not implemented / gaps: absent Cloudflare behavior or unsupported shapes.

Status Legend

  • Supported: expected to work for ordinary WDL tenant applications and covered by unit or integration tests.
  • Partial: implemented for the documented WDL model, but missing Cloudflare behavior, global edge semantics, or less-common configuration shapes.
  • Not supported: rejected at deploy/config time or intentionally absent.
  • Internal: platform-facing surface, not tenant API compatibility.

Runtime And Language Surface

Surface Status What workerd provides Stronger / added in WDL Different from Cloudflare Not implemented / gaps
WebAssembly modules Supported Native Wasm compilation and execution, including compact imports and opt-in js-string builtins and source URLs. CLI/Control preserve Wasm module bytes through immutable bundle storage and workerLoader. Modules compile under each worker version's compatibility flags. No WDL-provided WASI runtime; dynamic code-generation restrictions still apply.
ES module Workers and fetch() Supported Module evaluation, request dispatch, Response/Request, service binding JSRPC machinery. Dynamic workerLoader, immutable version ids, wrapper-generated env, gateway routing, request logs, and public/private outbound separation. Root module names beginning _wdl- are platform-reserved; new deploys are rejected and retained versions fail closed on cold load. An uncaught tenant fetch() exception maps to platform 502 runtime_error; exception detail goes to structured logs/live tail, not the client body. WDL does not emulate every compatibility-date behavior change; workerd is configured with the platform's enabled flags.
WebSocket upgrade Supported WebSocket API and 101 response handling inside workerd. Gateway terminates a local public WebSocket pair and directly forwards to runtime/do-runtime with bounded backend reconnect and client-frame buffering. Cloudflare disconnects every WebSocket when new code is deployed. Unless a worker opts into sessionPolicy: "restart" (which closes its sessions at promotion), WDL keeps gateway-proxied sockets across a promotion while the pinned version's backend stays healthy; only after that backend is lost does Gateway revalidate the active route and, if the pinned version is no longer active, close with 1012 so the client upgrades to the active version. Gateway rolling still drops physical client sockets; application-level resume is not implemented. Reconnect is application-owned: neither the WebSocket protocol nor the bare API defines a close code that triggers reconnection.
compatibility_date / compatibility_flags Partial workerd feature flags and compatibility behavior where configured in capnp. CLI/control stores bundle metadata. Dynamic Workers reject explicit compatibility_date values earlier than 2026-04-01; Control also validates that the value is a real YYYY-MM-DD date that is not later than the current UTC date or the maximum date supported by the bundled workerd, rejects upstream $experimental enable flags such as experimental / unsafe_module, and rejects legacy_error_serialization, allow_irrevocable_stub_storage, new_module_registry, no_rpc, and streams_disable_constructors. Redundant positive flags already enabled by date are preserved and accepted when they compile to the same flag set; workerd owns other incompatible combinations at cold load. WDL treats compatibility metadata as deploy-time platform metadata rather than a complete per-worker historical emulation layer. No per-worker emulation of every Cloudflare historical behavior; tenant workers cannot opt into upstream experimental-only flags, disable WDL-required enhanced error serialization, Fetcher RPC, or standard stream constructors, persist irrevocable capability stubs, or enable a module registry whose import.meta.main identity conflicts with WDL's generated main wrapper.
nodejs_compat Partial workerd provides Node.js compatibility modules and globals. Starting at compatibility date 2026-08-04, nodejs_compat and nodejs_compat_v2 are both enabled by default. CLI carries compatibility flags into metadata. A worker that needs the pre-2026-08-04 surface must set both no_nodejs_compat and no_nodejs_compat_v2; disabling only the first leaves the independently defaulted V2 surface enabled. WDL exposes the workerd-enabled compatibility surface rather than a separate Node.js runtime. This is not a full Node.js platform contract beyond workerd's enabled surface. net.BoundSocket only reserves an isolate-local virtual endpoint; it neither binds an OS listener nor sets the connection's source address.
Execution context and module time Partial workerd exposes ctx.abort(reason?), ctx.tracing.startSpan(), ctx.tracing.getActiveSpan(), Span.recordException(), chained span attribute setters, Span.updateName(), Span.setStatus(), Span.spanContext(), and production-like request time. Outside an active request, including top-level module evaluation, Date.now(), zero-argument new Date(), and performance.now() return 0. WDL passes the native execution context through generated wrappers and does not emulate wall time during module evaluation. ctx.abort() ends the current stateless invocation; it is distinct from WDL's host-only historical-isolate eviction. WDL's buffered live tail does not export spans; span export requires a streaming-tail sink that WDL does not provide. Span.spanContext() returns ids generated for the current invocation, with traceFlags 0; an inbound traceparent does not set them, and they are never exported.
Python Workers modules Not supported workerd's python_workers_314 is non-experimental and is implied by python_workers at compatibility dates on or after 2026-09-08; auto_inject_python_workers is experimental and python_workers_20260817 is removed. Control rejects py module manifests with python_workers_unsupported; runtime and do-runtime also fail closed for retained metadata containing py modules. WDL keeps tenant bundles JavaScript/WebAssembly/data only and does not permit cold-load-time Pyodide bootstrap. Python Workers and mixed JS/Python bundles are unsupported.

WDL does not generally guarantee workerd downgrades. As best-effort guidance, a target binary can cold-load a retained Dynamic Worker version only when it supports that version's compatibility_date; see the infra rollout notes.

ctx.mapVirtualHost(fetcher, port) is available without experimental flags. It maps an already-held native Fetcher to a virtual host and port for socket clients; it neither grants new network reachability nor turns a JavaScript binding facade into a TCP backend.

spec_compliant_dispatch_exceptions is non-experimental and accepted as an explicit opt-in at an earlier valid compatibility date. It keeps listener exceptions from escaping dispatchEvent() and allows later listeners to run. Its default is tied to a worker compatibility date of 2026-09-15 or later unless explicitly disabled, not the calendar date. The bundled workerd maximum is 2026-11-10; Control also rejects dates later than the current UTC date. WDL's static workers use 2026-04-24 without this opt-in.

auto_grpc_convert is non-experimental and accepted as compatibility metadata. It does not add Cloudflare's edge gRPC conversion service to WDL.

durable_object_io_tasks_prevent_eviction is non-experimental and defaults on for compatibility dates from 2026-10-01. Stock workerd does not consume it, so it does not change WDL actor residency; DO_PREVENT_EVICTION remains the residency control.

webcrypto_modern_algorithms is an accepted, non-experimental opt-in. It exposes workerd's native ML-KEM, ML-DSA, ChaCha20-Poly1305, MLKEM768-X25519, SHA3-256, SHA3-384, SHA3-512, and related WebCrypto helpers. The upstream API follows an evolving draft; WDL does not enable it by default or use it for platform secrets.

wasm_esm_integration_builtins is an accepted, non-experimental opt-in. It compiles a worker's Wasm modules with the js-string builtins and wasm:js/string-constants imports enabled; modules that do not import the wasm:* namespaces are unaffected. WDL delivers Wasm modules to workerLoader as bytes, so each version compiles them under its own flags.

Compact WebAssembly import-section encoding is also accepted by the bundled workerd without an opt-in. WDL forwards Wasm module bytes without rewriting them.

wasm_source_url_support preserves the module's path in native Wasm stack frames, including errors passed to structured live tail; without it, frames use an anonymous wasm://wasm/ URL. temporal exposes the native Temporal API. Both are accepted, non-experimental opt-ins and default on from compatibility date 2026-12-01, beyond the bundled maximum. Neither is enabled on WDL's static workers.

The native Response constructor defaults statusText to an empty string and rejects a locked body stream immediately. Explicit status text is preserved; HTTP status codes remain the wire contract.

pipe_to_backward_propagation is an accepted, non-experimental opt-in. It makes pipeTo() and pipeThrough() propagate a JavaScript-backed destination's errors and closure back to the source, including failures while canceling the source. cache_option_preserves_cache_control is also accepted; it stops the caller's fetch() cache option from synthesizing cf.cacheControl, while no-cache still sets cacheForceRevalidate. Synthesis from cf.cacheTtl is unchanged: forwarding a no-store request through another service-binding fetch can re-synthesize cacheControl: "no-store" from its cacheTtl: -1. This does not add a CDN cache to WDL. Both default on from compatibility date 2026-11-17, beyond the bundled maximum; neither is enabled on WDL's static workers.

Node.js TLS behavior follows the bundled workerd binary. Starting with WDL's 2026-07-01 workerd pin, workers whose compatibility date is at least 2026-06-16 get throw_on_not_implemented_tls_options: unsupported node:tls options such as checkServerIdentity now throw ERR_OPTION_NOT_IMPLEMENTED instead of being silently ignored. Separately, workerd's servername / expected-certificate-hostname behavior changed outside any compatibility flag, so certificate hostname validation follows the bundled workerd behavior for all dates.

Starting with WDL's 2026-08-11 workerd pin, byte-stream BYOB reads use live ArrayBufferView bounds instead of cached offsets and lengths, including when buffers are resized or transferred. The same pin adopts ada-url 4: an ASCII hostname label that starts with xn-- but does not decode to a valid UTS #46 label is preserved and lowercased instead of being rejected. WDL accepts such labels as literal route labels when they also satisfy its ASCII DNS grammar in shared/ns-pattern.js; numeric-host canonicalization remains in control/topology.js, and Unicode route names remain unsupported. For compatibility dates on or after 2026-08-11, workerd also enables workflows_enable_fast_engine_creation, changing how its native Workflow engine allocates instance IDs. WDL does not use that engine; its custom facade and DB 2-backed workflows service retain WDL-owned instance identity and lifecycle semantics.

Compatibility Flag Policy

Compatibility flags are part of the architecture toolbox, not a uniformly forbidden surface. WDL evaluates them by who executes the code and what capability the flag adds:

  • Platform-owned static workers should proactively evaluate explicit flags when the native workerd behavior removes WDL code, makes a trust boundary smaller, or avoids a parallel implementation. Pin the flag in the owning Cap'n Proto service, audit the upstream implementation and future compatibility implications, and cover the enabled behavior with real-workerd tests.
  • Tenant Dynamic Worker flags are intentionally more conservative. Enabling one changes a public execution contract across arbitrary tenant code and retained versions. It needs an explicit product decision, Control and retained-state policy, interaction review with generated wrappers and binding isolation, and a rollout plan where required.
  • A flag must be judged by its complete capability and trust-zone blast radius, not only by whether it makes one call site easier. A platform-only flag may be appropriate even when the analogous tenant flag is denied. Conversely, do not expose a broad tenant capability merely to solve a narrow host-plumbing problem.

Bundled workerd permits Fetcher and Durable Object class stubs to cross JSRPC as opaque arguments without an experimental flag. WDL treats possession of such a stub as capability delegation: the receiver can call the delegated target with the host-authored caller properties embedded in that stub, but cannot rewrite those properties or recover hidden platform backend capabilities. This delegation may live in memory. Under the current design, WDL rejects allow_irrevocable_stub_storage at deploy and retained-state load, and static host workers do not enable it, because the tenant-visible persistent-stub surface is broader than WDL's current binding contracts. This is a scoped capability decision under the policy above, not a blanket rule against using compatibility flags in platform workers.

Connected cloudflare:sockets sockets can also be transferred through service-binding JSRPC. This delegates the existing connection, including its readable and writable streams; it does not grant the receiver authority to open arbitrary new connections. The creator's outbound restrictions still apply when establishing the connection. Privileged workers must not delegate private connections to untrusted code.

Bindings And Storage

Surface Status What workerd provides Stronger / added in WDL Different from Cloudflare Not implemented / gaps
KV namespace Supported A binding object can be exposed to user code through workerd entrypoint/JSRPC mechanics. Runtime KV facade calls redis-proxy; redis-proxy stores values and metadata in DB 1 hash buckets kvh:<ns>:<id>:b:<bucket>, with v:<key> and m:<key> fields, 512-byte key/list-prefix cap, 1024-byte serialized metadata cap, 25 MiB value cap, batch raw-byte budget, TTL/EXAT, and prefix list cursors. KV storage is Redis-backed and namespace-scoped in a WDL deployment. cacheTtl is not a storage freshness contract. No Cloudflare global edge replication or eventual consistency model.
R2 bucket Supported Fetch/stream primitives in workerd. Runtime R2 facade signs S3-compatible requests with platform credentials; CLI parses [[r2_buckets]]. Bucket lifecycle and placement are the S3-compatible backend's responsibility. Headers-form httpMetadata accepts Expires only as a canonical IMF-fixdate and rejects malformed values before the host call. preview_bucket_name and jurisdiction are not supported.
Workers AI binding Partial workerd supplies WorkerEntrypoint/JSRPC, Fetcher, streams, and WebSockets. WDL adds namespace-scoped encrypted BYO credentials, official OpenAI/xAI/DeepSeek adapters, fetch()/run()/models(), OpenAI-compatible JSON/SSE, Responses and Realtime WebSockets, public-only egress, and bounded host pools. This is a namespace-owned provider registry and credential boundary, not Cloudflare Workers AI infrastructure or a WDL-managed catalog. Model ids use <provider>/<alias> and the raw surface uses https://ai.wdl/v1. No arbitrary endpoint, managed catalog, platform credential, usage/quota/billing, toMarkdown(), async batch, background Responses, WebRTC, or automatic tool execution. DeepSeek is HTTP/SSE only.
ASSETS Partial Worker can receive a platform-provided binding object. CLI uploads assets to S3-compatible storage and runtime exposes the WDL env.ASSETS.url(path) helper for tokenized CDN URLs. WDL assets are an S3/CDN helper model, not Cloudflare Pages asset hosting. WDL does not provide a full Cloudflare Pages asset pipeline or a fetch-style assets binding contract.
D1 database Partial workerd can host a D1-like binding facade and localDisk-backed SQLite actor code; the bundled SQLite includes R*Tree virtual tables and rtreecheck(). WDL implements control-plane metadata, d1-runtime, owner lease/generation fencing, migrations, SQL execution, drain/renew, deploy-time alias freezing, and caps on query bodies, decoded statement payloads, rows, and result bytes before SQLite work or response emission. D1 storage is WDL-owned SQLite with owner routing, not Cloudflare global D1. Physical SQLite files are not deleted on metadata delete. No Cloudflare global replication/bookmark semantics. SQLite names under the reserved _cf_ namespace are rejected by workerd case-insensitively.
Durable Objects Partial Native Durable Object class execution, facet identity, SQLite-backed ctx.storage.sql including R*Tree virtual tables, and in-facet WebSocket hibernation API. WDL implements runtime facade, do-runtime owner routing, shard leases, Redis generation fencing, alarm shim with Workflows-backed due/retry jobs, gateway-managed public WebSocket forwarding, storage ids, cleanup tombstones, and bundle-owned preserve/restart session policy. preserve is the default; restart closes superseded public WebSockets with 1012 and lazily aborts stale facets without deleting SQLite. Deployment-level DO_PREVENT_EVICTION defaults to true, keeping host actors resident; explicit false permits idle actor reconstruction while preserving SQLite and quiescent hibernatable socket state. WDL DO identity, owner routing, session policy, cleanup, and residency policy are WDL-managed rather than Cloudflare migration-compatible. restart matches Cloudflare's default, where deploying new code restarts every Durable Object and disconnects its WebSockets; preserve is the WDL default and has no Cloudflare equivalent. A later preserve promotion can supersede restart work not yet observed. Same-worker classes only. script_name, rename/delete migrations, and platform-level WebSocket session recovery are not implemented. Actor-local JavaScript state does not survive eviction. Tenant ctx.storage.deleteAll() is a WDL best-effort KV/SQL/Workflows sequence rather than Cloudflare's atomic storage operation; rejection may leave a partial result, and concurrent mutation is unsupported. Alarm mutation spans object SQLite and Workflows DB 2; after backend mutation starts, rejected setAlarm() or deleteAlarm() has unknown final state and requires a successful operation-specific retry. getAlarm() repair is best effort and does not confirm backend state. Native ctx.abort(..., { retryAlarm: false }) does not suppress retries for WDL's Workflows-backed alarm dispatch. Current workerd can still lose an in-flight hibernatable WebSocket application send or close across actor eviction, so eviction remains explicit opt-in; workerd 2026-08-25 fixes the former in-flight auto-response loss. SQLite names under the reserved _cf_ namespace are rejected by workerd case-insensitively.
Queues producer/consumer Partial Worker queue handler API surface in loaded workers. Runtime producer facade writes through redis-proxy DB 1; scheduler owns consumer dispatch, retry, DLQ, orphan, delayed queues, and batch splitting for oversized dispatch bodies. max_batch_timeout_ms is configuration metadata, not a true aggregation window; dispatch concurrency is scheduler-owned. max_concurrency is rejected.
Cron triggers Supported scheduled() handler surface in workers. Control stores cron config; scheduler owns indexed discovery, cron-slot buckets, due dispatch, and repairable projections. Control and scheduler use JS/Rust croner engines and keep their behavior documented through tests and module docs. New scheduler dispatch paths still require their own Redis lease/fence audit.
Workflows Partial User workflow class code runs inside loaded workers when dispatched by runtime. workflows owns DB 2 instance state, step/event/sleep commits, runtime-observed step.do DAG edges including Promise.all siblings, generation/run-token fencing, lifecycle APIs, progress callbacks, and scheduler tick. Payloads are JSON data under WDL byte caps; permanent step.do failure is terminal for the run even if caught, and one step may record at most 1000 dependency edges. Native workerd WorkflowInstance.delete(), Workflow.deleteBatch(), instance event subscriptions (subscribe()), rollback APIs, and restarting from a selected step (restart({ from: ... })) are not exposed by WDL's custom facade. Rollback and restart-from options are rejected rather than downgraded to ordinary operations. locationHint, script_name, cross-worker workflows, and Cloudflare's source-AST visualizer are also unsupported.
Service bindings Supported workerd service binding JSRPC and fetch dispatch. WDL resolves target worker metadata, cold-loads immutable versions, propagates request ids where available, and enforces namespace/action ACL through control metadata. Frozen-version service targets do not evict active siblings by design. Object-style custom RPC has no general generated env facade; use class-based WorkerEntrypoint methods with this.env when binding facades are required.
Platform bindings Supported workerd named entrypoint/JSRPC mechanics. WDL expands ACL-checked platform bindings from control metadata. Platform bindings are WDL-specific. Platform bindings are not a Cloudflare tenant portability feature.
Vars and secrets Supported Env values can be materialized into worker env. Control stores worker vars/secrets metadata, encrypts secret values as WDL-ENC: envelopes at rest, redis-proxy decrypts them during cold-load, enforces a headroomed workerd 1 MiB workerLoader serialized env budget for user vars/secrets plus runtime-injected binding env values and binding-scoped Workflow identity props during deploy/secret mutation, accounts for V8 two-byte string storage for non-Latin-1 strings, and immutable versions are promoted when worker secrets change. Secrets are platform-managed by WDL; the at-rest envelope provider is a deployment concern. WDL secrets are not Cloudflare account secrets.
Worker code size Supported Dynamic worker module bodies are accepted by workerLoader up to workerd's 64 MiB limit. Control estimates final WorkerCode, including runtime/do-runtime injected wrapper/client modules and workflow import rewrites, before writing a deployed version. WDL's deploy JSON body limit is lower for ordinary inline deployments. Large server-side bundle assembly paths must keep this guard.

D1 and Durable Object SQL DEFAULT expressions obey workerd's existing function authorization, including defaults in retained schemas. See the D1 and Durable Objects storage contracts.

Control Plane And Developer Tooling

Surface Status What workerd provides Stronger / added in WDL Different from Cloudflare Not implemented / gaps
Wrangler project parsing Partial None. WDL CLI parses the supported subset of wrangler.toml / JSONC: KV, D1, R2, AI, services, DO, workflows, queues, vars, assets, routes, and explicit workers_dev platform-domain opt-out. Unsupported fields are rejected rather than silently emulated. Cloudflare uses workers_dev for the Worker's *.workers.dev route; versioned preview URLs are controlled separately by preview_urls, which defaults to workers_dev. WDL maps workers_dev to <ns>.<platform-domain>/<worker>/, the namespace's ordinary serving path, and does not support preview_urls. A project that disables its Cloudflare *.workers.dev route therefore disables a production entry point here, so WDL requires at least one route pattern before accepting the opt-out. Per-binding rows above call out the major rejected shapes.
Worker deploy/promote/delete Supported workerd loads what the platform supplies. Control/auth own bundle commit, route promotion, WATCH/MULTI fences, lifecycle indexes, retained versions, secrets, and async S3 cleanup intents. WDL API/CLI is the management surface. Cloudflare API parity is not the goal.
Log tail Supported Worker console output and positional Error metadata are available to the runtime tail worker. Runtime emits bounded structured logs and preserves workerd errorInfo as error_info; control authorizes tail sessions and redis-proxy stores bounded streams. Tail activation is time-bounded. Messages racing activation can be dropped; buffered live tail does not export tracing spans.
Metrics/health Supported Service code can expose HTTP endpoints. Gateway, runtime, d1-runtime, do-runtime, scheduler, workflows, and redis-proxy expose their service-specific probes/metrics as documented in module docs. Metrics sockets are per-service. Control/auth have no standalone public metrics socket.

Unsupported Or Not Yet Modeled Cloudflare Surfaces

These entries are intentionally explicit so compatibility gaps do not hide behind module docs:

Surface Status Current WDL position
Cache API / Cloudflare edge cache semantics Not supported caches.default is not part of the stock workerd surface WDL exposes, and WDL does not implement Cloudflare's edge cache tier or the embedder service behind ctx.cache.purge() / invalidate(). Tenant code should not depend on these as a persistence/CDN contract.
Vectorize, Analytics Engine / Analytics SQL, Browser Rendering, Hyperdrive, Email Workers, Artifacts, Flagship Not supported No binding facade, control-plane metadata, or backing service exists in WDL.
R2 multipart upload, customer-provided encryption keys, and Cloudflare-specific checksum behavior Not supported The current R2 facade targets S3-compatible object operations needed by WDL workers/assets. Advanced Cloudflare R2 behaviors need explicit design before being documented as compatible.
Queue contentType = "v8" and per-consumer max_concurrency Not supported Queue messages support the documented json, text, and bytes content types; only v8 is rejected. Dispatch concurrency remains scheduler-owned, and max_concurrency is rejected instead of silently ignored.
Incoming TCP/UDP connect() handlers Not supported WDL configures no tenant raw-TCP or raw-UDP ingress. UDP also requires the worker experimental flag, which WDL rejects for tenants. This does not affect outbound cloudflare:sockets or delegation of an existing socket over JSRPC.
Upstream experimental and WDL-denied compatibility flags Not supported Tenant compatibility_flags entries whose upstream workerd flag is marked $experimental, plus WDL's explicit deny policies for allow_irrevocable_stub_storage, new_module_registry, no_rpc, and streams_disable_constructors, are rejected at deploy and runtime decode. New Module Registry has graduated upstream, but WDL's generated wrapper is the actual dynamic-loader main module and cannot preserve tenant import.meta.main; Fetcher RPC and standard stream constructors are required by WDL runtime facades.
Python Workers Not supported WDL rejects Python module manifests instead of letting workerd fail at cold-load.
Durable Object cross-script bindings and migration rename/delete semantics Not supported WDL DO classes are same-worker only. Storage identity, owner routing, and delete cleanup are WDL-managed rather than Cloudflare migration-compatible.
Durable Object snapshots and user-configured native retries Not supported Native snapshot() / onNextSessionRestore() require the rejected durable_object_snapshots or catch-all experimental flag and an embedder storage implementation. The cloudflare:durable-objects retryable() marker does not enable retries: its native userland gate is off, and WDL retains its own owner-fenced fetch/RPC and alarm contracts.
Workflow createBatch({ count, ... }) / createBatch({ instances }) Not supported WDL accepts the non-empty array form only and rejects object overloads before backend I/O. The native partial-success object result is not a WDL contract.
Cloudflare account API parity Not supported WDL exposes its own CLI/control API. Cloudflare API compatibility is not a stated goal.

Design Rule

When workerd already provides an in-isolate programming model, WDL tries to keep that programming model visible to tenant code. When Cloudflare's production platform supplies an external service, WDL must implement the missing external piece: control metadata, Redis/S3 storage adapters, owner routing, scheduler dispatch, or lifecycle cleanup. Compatibility work should therefore state both halves: the workerd surface being reused and the WDL service that supplies the platform behavior around it.