SYSTEM Cited by 1 source
Cloudflare Dynamic Workflows¶
Dynamic Workflows (@cloudflare/dynamic-workflows) is
Cloudflare's dispatch library that
bridges Workflows (durable
execution) with Dynamic Workers
(per-request V8-isolate sandboxing), letting a single Worker
Loader route every create() call and every subsequent
run(event, step) invocation to a different tenant's code
(github.com/cloudflare/dynamic-workflows,
npm install @cloudflare/dynamic-workflows).
MIT-licensed, ~300 lines of TypeScript. Launched 2026-05-01.
What problem it solves¶
wrangler.jsonc statically binds each Workflow class_name to a
single class per deploy:
"workflows": [
{
"name": "dynamic-workflow",
"binding": "WORKFLOW",
"class_name": "DynamicWorkflow"
}
]
One binding, one class. That's fine if you own all the code.
It breaks the moment you want your customer / tenant /
repo / agent to ship their own run(event, step)
function — app platforms where the AI writes TypeScript per
tenant, multi-tenant SaaS with customer-defined business logic,
CI/CD products where each repo has its own pipeline, agents
SDKs where each agent writes its own durable plan. Dynamic
Workflows lifts the one-class-per-deploy constraint.
Three-layer dispatch topology¶
┌──────────────────────────────────────┐
│ Workflows engine │ (platform)
│ (persistence + replay + step │
│ retries + sleep + waitForEvent) │
└──────────────────────────────────────┘
↑ ↓ run(event, step)
┌──────────────────────────────────────┐
│ Worker Loader │ (you)
│ — fetch, route, wrapWorkflowBinding │
│ — createDynamicWorkflowEntrypoint │
└──────────────────────────────────────┘
↑ ↓ RPC
┌──────────────────────────────────────┐
│ Tenant code (Dynamic Worker) │ (your customer)
│ class TenantWorkflow extends │
│ WorkflowEntrypoint { run(...) } │
└──────────────────────────────────────┘
The Worker Loader sits in the middle. The engine is above and unchanged. The tenant is below and unchanged — they write "plain, idiomatic Workflows code. They have no idea they're being dispatched." (Source: Dynamic Workflows post.)
Core mechanism: wrap .create() out, wrap WorkflowEntrypoint in¶
Every load-bearing line of the library is either an outbound wrapper or an inbound wrapper.
Outbound (wrapWorkflowBinding)¶
The env.WORKFLOWS the tenant sees is a
wrapWorkflowBinding({ tenantId }) specialisation, not the real
Workflow binding. When the tenant calls env.WORKFLOWS.create(...),
the wrapped binding rewrites the payload:
tenant calls: create({ params: { name: 'Alice' } })
│
▼
engine sees: create({ params: {
__workerLoaderMetadata: { tenantId: 't-42' },
params: { name: 'Alice' }
}})
…and forwards to the real WORKFLOWS binding. The engine
persists the envelope; the metadata rides along with the payload
through every sleep, crash, and redeploy. Canonicalised here as
concepts/envelope-wrap-and-unwrap-metadata-routing and
patterns/metadata-envelope-in-durable-payload.
Inbound (createDynamicWorkflowEntrypoint)¶
You register exactly one class in wrangler.jsonc. When the
engine wakes the workflow — seconds, hours, or days later — it
calls .run(event, step) on that registered class. The class
unwraps the envelope, hands the metadata to the loadRunner
callback you wrote, and forwards the unwrapped event through
to whatever runner the callback returns:
export const DynamicWorkflow = createDynamicWorkflowEntrypoint<Env>(
async ({ env, metadata }) => {
const stub = loadTenant(env, metadata.tenantId);
return stub.getEntrypoint('TenantWorkflow');
}
);
The loadRunner callback is where "everything interesting
happens, and it's entirely yours": fetch tenant source from R2,
check plan tier, pick a region, attach a tail Worker for
per-tenant logging, bundle TypeScript on the fly with
@cloudflare/worker-bundler. Canonicalised here as
concepts/byo-workflow-per-tenant.
Why the binding must be an exported RPC class¶
Bindings that cross the Dynamic Worker boundary must be RPC
stubs. "A plain { create, get } object can't be
structured-cloned, and the raw Workflow binding isn't
serializable either." (Source:
sources/2026-05-01-cloudflare-introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.)
The wrapped binding is a WorkerEntrypoint subclass
(DynamicWorkflowBinding) that the runtime specialises with the
tenant's metadata at load time. That's why you must
export { DynamicWorkflowBinding } from your Worker Loader — the
runtime builds per-tenant stubs by looking the class up in
cloudflare:workers exports.
The full minimal Worker Loader¶
import {
createDynamicWorkflowEntrypoint,
DynamicWorkflowBinding,
wrapWorkflowBinding,
} from '@cloudflare/dynamic-workflows';
// The library looks this class up on cloudflare:workers exports.
export { DynamicWorkflowBinding };
function loadTenant(env, tenantId) {
return env.LOADER.get(tenantId, async () => ({
compatibilityDate: '2026-01-01',
mainModule: 'index.js',
modules: { 'index.js': await fetchTenantCode(tenantId) },
// The tenant sees this as a normal Workflow binding.
env: { WORKFLOWS: wrapWorkflowBinding({ tenantId }) },
}));
}
export const DynamicWorkflow = createDynamicWorkflowEntrypoint<Env>(
async ({ env, metadata }) => {
const stub = loadTenant(env, metadata.tenantId);
return stub.getEntrypoint('TenantWorkflow');
}
);
export default {
fetch(request, env) {
const tenantId = request.headers.get('x-tenant-id');
return loadTenant(env, tenantId).getEntrypoint().fetch(request);
},
};
The tenant's code is unchanged¶
import { WorkflowEntrypoint } from 'cloudflare:workers';
export class TenantWorkflow extends WorkflowEntrypoint {
async run(event, step) {
return step.do('greet', async () => `Hello, ${event.payload.name}!`);
}
}
export default {
async fetch(request, env) {
const instance = await env.WORKFLOWS.create({ params: await request.json() });
return Response.json({ id: await instance.id });
},
};
Workflow IDs, .status(), .pause(), retries, hibernation,
durable steps, step.sleep('24 hours'), step.waitForEvent() —
all work unchanged. The tenant has no idea they're being
dispatched.
Isolate caching + hibernation economics¶
- Dynamic Worker boot: single-digit milliseconds.
- Dynamic Worker memory: a few megabytes.
- Cache key: tenant ID (via
env.LOADER.get(tenantId, ...)). A workflow that runs many steps over many hours reuses the same dynamic Worker across them. - Eviction recovery: when the isolate is evicted, the next
step.do()pulls the code again and keeps going — invisible to the workflow. - Idle cost: approximately zero. "You can have a million tenants, each with their own distinct workflow code, each spun up lazily on the step boundary where it's needed, and none of them cost anything while idle." (Source: sources/2026-05-01-cloudflare-introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.)
Metadata is a routing hint, not authorization¶
"The tenant can read it back via instance.status(). Don't put
secrets in there." (Source:
sources/2026-05-01-cloudflare-introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.)
Tenant isolation / authorization remains the Worker Loader's
responsibility — via Dynamic Workers'
capability-based bindings
(globalOutbound: null + explicit capabilities resource-by-
resource).
Lower-level primitive: dispatchWorkflow¶
If you want to subclass WorkflowEntrypoint yourself — to add
logging around run(), wire up per-tenant observability, or
thread custom state through — the library exposes the
lower-level dispatchWorkflow callable that
createDynamicWorkflowEntrypoint is built on:
import { dispatchWorkflow } from '@cloudflare/dynamic-workflows';
export class MyDynamicWorkflow extends WorkflowEntrypoint {
async run(event, step) {
return dispatchWorkflow(
{ env: this.env, ctx: this.ctx },
event,
step,
({ metadata, env }) => loadRunnerForTenant(env, metadata),
);
}
}
Positioning: one of three dynamic-binding primitives¶
Cloudflare frames Dynamic Workflows as the third instance of a generalising pattern:
| Layer | Static binding | Dynamic binding |
|---|---|---|
| Compute | Workers | Dynamic Workers |
| Storage | Durable Objects | Durable Object Facets |
| Durable execution | Workflows | Dynamic Workflows (this page) |
| Queues / caches / databases / object stores / AI bindings / MCP servers | static | pre-announced ("coming") |
Canonicalised in patterns/dynamic-binding-over-static-binding.
Canonical showcase: CI/CD as per-repo durable workflow¶
The customer ships .cloudflare/ci.ts — their own
CIPipeline extends WorkflowEntrypoint class. The platform
ingests a webhook, figures out which repo it came from, loads
that repo's CIPipeline as a Dynamic Worker, and hands
execution off to Dynamic Workflows. The platform doesn't know
what's in the pipeline — "it doesn't need to. It's running a
durable function that happens to live in the customer's repo."
(Source:
sources/2026-05-01-cloudflare-introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.)
- Artifacts +
ArtifactFS
fork()— per-run isolated repo copy in single-digit seconds, nogit clonetax. - Dynamic Workers — each lightweight step (lint, format, typecheck, bundle) in a millisecond-boot isolate on the same machine as the repo.
- Dynamic Workflows — holds the run together; steps are retryable + durable; hibernation free while waiting on approvals; state + progress survive deploys / evictions / crashes.
- Sandboxes — heavy corners
(
docker build, integration suites that need Postgres, Rust 8-core compiles). Snapshot-restore to R2 means these warm- start in a couple of seconds.
Canonicalised as patterns/ci-pipeline-as-customer-authored-durable-workflow.
Workflows V2 capacity context¶
Dynamic Workflows runs atop the Workflows V2 rewrite, "redesigned for the agentic era":
- 50,000 concurrent workflow instances per account.
- 300 new instances per second per account.
These appear as ambient capacity context for a single-account platform running per-tenant workflows at fleet scale. First wiki disclosure of the V2 capacity envelope.
Role in the agent-autonomy stack¶
For coding agents — OpenCode, Claude Code, Codex, Pi — Dynamic
Workflows is "the piece that lets that plan be a first-class
Cloudflare Workflow". An agent literally writes a run(event,
step) body; the platform runs it with full durability
(step.do() retryable, step.sleep('24 hours') free
hibernation, step.waitForEvent() indefinite waits for human
approval). Extends Project Think's
fiber-based durable execution model with a
workflow-as-artifact primitive the model itself can emit.
What's not disclosed¶
- No p50/p99 latency numbers for the dispatch hop.
- No cost disclosure for the tens-of-millions-of-tenants unit-economics claim.
- No discussion of tenant code update semantics mid-workflow
(if a workflow sleeps 24 h and the tenant redeploys, does
run()resume on the new code or the old? Cache-by-ID implies eviction brings in the new code, but the determinism implications for replay are not discussed). - No comparison against prior-art external dispatchers
(Temporal dynamic task queues, Cadence namespaces,
serverless-workflow
@ref-loaded definitions). - No production adopters disclosed at launch — Day 1.
Seen in¶
Related¶
- systems/cloudflare-workflows
- systems/dynamic-workers
- systems/cloudflare-workers
- systems/durable-object-facets
- systems/cloudflare-durable-objects
- systems/cloudflare-artifacts
- systems/artifact-fs
- systems/cloudflare-sandbox-sdk
- systems/cloudflare-agents-sdk
- systems/project-think
- concepts/per-tenant-dynamic-code-dispatch
- concepts/envelope-wrap-and-unwrap-metadata-routing
- concepts/byo-workflow-per-tenant
- concepts/durable-execution
- concepts/capability-based-sandbox
- patterns/dynamic-binding-over-static-binding
- patterns/metadata-envelope-in-durable-payload
- patterns/ci-pipeline-as-customer-authored-durable-workflow
- patterns/workflow-primitives-as-annotated-classes
- companies/cloudflare