{"slug": "introducing-dynamic-workflows-durable-execution-that-follows-the-tenant", "title": "Introducing Dynamic Workflows: durable execution that follows the tenant", "summary": "Cloudflare has introduced Dynamic Workflows, a new feature that bridges durable execution with dynamic deployment, allowing platforms to run tenant-specific workflow code that is loaded at runtime rather than being part of the initial deployment. The solution uses a small TypeScript library called `@cloudflare/dynamic-workflows`, which enables a single Worker Loader to route workflow creation and execution calls to different tenant code, even when execution occurs hours or days later. This addresses the limitation of traditional Workflows, which required workflow code to be defined at deploy time, making it unsuitable for multi-tenant applications where each customer has unique business logic.", "body_md": "When we first launched Workers eight years ago, it was a direct-to-developers platform. Over the years, we have expanded and scaled the ecosystem so that platforms could not only build on Workers directly, but they could also enable *their* customers to ship code to *us *through many multi-tenant applications. We now see on Workers: Applications where users describe what they want, and the AI writes the implementation. Multi-tenant SaaS where every customer's business logic is, at runtime, some TypeScript the platform has never seen before. Agents that write and run their own tools. CI/CD products where every repo defines its own pipeline.\n\nLast month, when we shipped the __Dynamic Workers open beta__, we gave those platforms a clean primitive for the *compute* side: hand the Workers runtime some code at runtime, get back an isolated, sandboxed Worker, on the same machine, in single-digit milliseconds. __Durable Object Facets__ extended the same idea to *storage* — each dynamically-loaded app can have its own SQLite database, spun up on demand, with the platform sitting in front, as a supervisor. __Artifacts__ did the same for *source control*: a Git-native, versioned filesystem you can create by the tens of millions, one per agent, one per session, one per tenant. So, we have dynamic deployment for storage and source control. What’s next?\n\nToday, we are bridging durable execution and dynamic deployment with __Dynamic Workflows__.\n\n## The gap between durable and dynamic execution\n\n__Cloudflare Workflows__ is our durable execution engine. It turns a `run(event, step)`\n\nfunction into a program where every step survives failures, can sleep for hours or days, can wait for external events, and resumes exactly where it left off when the isolate is recycled. It's the right primitive for anything that has to \"keep going\" past a single request: onboarding flows, video transcoding pipelines, multi-stage billing, long-running agent loops, and — as of __Workflows V2__ — up to 50,000 concurrent instances and 300 new instances per second per account, redesigned for the agentic era.\n\nBut Workflows has always had one assumption baked in: the workflow code is part of your deployment. Your `wrangler.jsonc`\n\nhas a block that says *\"when the engine calls into **WORKFLOWS*\n\n*, run the class called **MyWorkflow*\n\n*.\"* One binding, one class. Per deploy.\n\nThat works fine if you own all the code. It's fine if you're running a traditional application.\n\nIt stops working the moment you want to let your customer ship *their* workflow.\n\nSay you're building an app platform where the AI writes TypeScript for every tenant. Say you're running a CI/CD product where each repository has its own pipeline. Say you're using an agents SDK where each agent writes its own durable plan. In every one of these cases, the workflow is different for every tenant, every agent, every request. There is no single class to bind.\n\nThis is the same shape of problem that Dynamic Workers solved for compute and that Durable Object Facets solved for storage. We just hadn't solved it for durable execution yet.\n\n`@cloudflare/dynamic-workflows`\n\nis a small library. Roughly 300 lines of TypeScript. It lets a single Worker — the **Worker Loader** — route every `create()`\n\ncall to a different tenant's code, and, critically, have the Workflows engine dispatch `run(event, step)`\n\nback to that same code when the workflow actually executes, seconds or hours or days later.\n\nHere's the whole pattern. A Worker Loader:\n\n``` js\nimport {\n  createDynamicWorkflowEntrypoint,\n  DynamicWorkflowBinding,\n  wrapWorkflowBinding,\n} from '@cloudflare/dynamic-workflows';\n\n// The library looks this class up on cloudflare:workers exports.\nexport { DynamicWorkflowBinding };\n\nfunction loadTenant(env, tenantId) {\n  return env.LOADER.get(tenantId, async () => ({\n    compatibilityDate: '2026-01-01',\n    mainModule: 'index.js',\n    modules: { 'index.js': await fetchTenantCode(tenantId) },\n    // The tenant sees this as a normal Workflow binding.\n    env: { WORKFLOWS: wrapWorkflowBinding({ tenantId }) },\n  }));\n}\n\n// Register this as class_name in wrangler.jsonc.\nexport const DynamicWorkflow = createDynamicWorkflowEntrypoint<Env>(\n  async ({ env, metadata }) => {\n    const stub = loadTenant(env, metadata.tenantId);\n    return stub.getEntrypoint('TenantWorkflow');\n  }\n);\n\nexport default {\n  fetch(request, env) {\n    const tenantId = request.headers.get('x-tenant-id');\n    return loadTenant(env, tenantId).getEntrypoint().fetch(request);\n  },\n};\n```\n\nAdd to your `wrangler.jsonc`\n\n:\n\n```\n\"workflows\": [\n\t\t{\n\t\t\t\"name\": \"dynamic-workflow\",\n\t\t\t\"binding\": \"WORKFLOW\",\n\t\t\t\"class_name\": \"DynamicWorkflow\"\n\t\t}\n\t]\n```\n\nThe tenant writes plain, idiomatic Workflows code. They have no idea they're being dispatched:\n\n``` js\nimport { WorkflowEntrypoint } from 'cloudflare:workers';\n\nexport class TenantWorkflow extends WorkflowEntrypoint {\n  async run(event, step) {\n    return step.do('greet', async () => `Hello, ${event.payload.name}!`);\n  }\n}\n\nexport default {\n  async fetch(request, env) {\n    const instance = await env.WORKFLOWS.create({ params: await request.json() });\n    return Response.json({ id: await instance.id });\n  },\n};\n```\n\nThat's it. The tenant calls `env.WORKFLOWS.create(...)`\n\nagainst what looks like a perfectly normal Workflow binding. Workflow IDs, `.status()`\n\n, `.pause()`\n\n, retries, hibernation, durable steps, `step.sleep('24 hours')`\n\n, `step.waitForEvent()`\n\n— everything works the way it always has.\n\nThe library handles one thing: making sure that when the Workflows engine eventually wakes up and calls `run(event, step)`\n\n, it ends up inside the *right tenant's* code.\n\nThree layers: the Workflows engine (platform) on top, your Worker Loader in the middle, your tenant's code (a Dynamic Worker) on the bottom.\n\nWhen a request reaches the Worker Loader, it routes the execution to the correct dynamic code on the fly. The rest of the execution is a handoff between these three layers, left-to-right in time: the request enters, bounces up to the engine, is persisted, and later bounces back down again.\n\nWalking the flow:\n\n**① → ② Entering the tenant's code.** The Worker Loader receives an HTTP request, figures out which tenant it's for, loads that tenant's code via the Worker Loader, and forwards the request to its `default.fetch`\n\n. The `env`\n\nit hands the tenant contains `WORKFLOWS: wrapWorkflowBinding({ tenantId })`\n\n. As far as the tenant is concerned, that looks and acts like a real Workflow binding.\n\n**③ Up to the Worker Loader.** When the tenant calls `env.WORKFLOWS.create({ params })`\n\n, it's actually making a Remote Procedure Call (RPC) into the Worker Loader — the wrapped binding is a `WorkerEntrypoint`\n\nsubclass (`DynamicWorkflowBinding`\n\n) that the runtime specialized with the tenant's metadata at load time. That's why you have to `export { DynamicWorkflowBinding }`\n\nfrom your Worker Loader: the runtime builds per-tenant stubs by looking the class up in `cloudflare:workers`\n\nexports. Bindings that cross the Dynamic Worker boundary *have* to be RPC stubs — a plain `{ create, get }`\n\nobject can't be structured-cloned, and the raw `Workflow`\n\nbinding isn't serializable either.\n\nInside the Worker Loader, the wrapped binding transparently rewrites the payload:\n\n```\ntenant calls:  create({ params: { name: 'Alice' } })\n                            │\n                            ▼\nengine sees:   create({ params: {\n                  __workerLoaderMetadata: { tenantId: 't-42' },\n                  params: { name: 'Alice' }\n               }})\n```\n\n**④ Up to the engine.** The Worker Loader then calls `.create()`\n\non the *real* `WORKFLOWS`\n\nbinding with the envelope as the params. From here the Workflows engine takes over. It persists `event.payload`\n\n— which now includes the envelope — and schedules the run. Every time the engine later wakes up the workflow (whether that’s after a 24-hour sleep, a crash, or a deploy), the metadata rides along with the payload, waiting to route the run.\n\nOne implication: treat the metadata as a routing hint, not as authorization. The tenant can read it back via `instance.status()`\n\n. Don't put secrets in there.\n\n**⑤ → ⑥ The engine comes back down.** When the engine is ready to run a step, it calls `.run(event, step)`\n\non the class you registered in `wrangler.jsonc`\n\n— the one `createDynamicWorkflowEntrypoint`\n\ngave you. That class unwraps the envelope, hands the metadata to the `loadRunner`\n\ncallback *you* wrote, and forwards the unwrapped event through to whatever runner the callback returns.\n\nThe callback is where everything interesting happens, and it's entirely yours. Fetch the tenant's latest source from R2. Check their plan tier and pick a region. Attach a tail Worker for per-tenant logging. Bundle TypeScript on the fly with __@cloudflare/worker-bundler__\n\n. In the common case, you just hand off to the Worker Loader:\n\n``` js\nconst stub = env.LOADER.get(tenantId, () => loadTenantCode(tenantId));\nreturn stub.getEntrypoint('TenantWorkflow');\n```\n\nThe Worker Loader caches by ID, so a workflow that runs many steps over many hours reuses the same dynamic Worker across them. When the isolate eventually gets evicted, the next `step.do()`\n\npulls the code again and keeps going — the tenant's workflow has no idea anything happened. A Dynamic Worker boots in single-digit milliseconds using a few megabytes of memory, so the dispatch overhead is essentially free. 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.\n\nIf you want to subclass `WorkflowEntrypoint`\n\nyourself — to add logging around `run()`\n\n, wire up per-tenant observability, or thread custom state through — the library exposes the lower-level `dispatchWorkflow`\n\nprimitive that `createDynamicWorkflowEntrypoint`\n\nis built on:\n\n``` js\nimport { dispatchWorkflow } from '@cloudflare/dynamic-workflows';\n\nexport class MyDynamicWorkflow extends WorkflowEntrypoint {\n  async run(event, step) {\n    return dispatchWorkflow(\n      { env: this.env, ctx: this.ctx },\n      event,\n      step,\n      ({ metadata, env }) => loadRunnerForTenant(env, metadata),\n    );\n  }\n}\n```\n\nEverything else — IDs, pause/resume, `sendEvent`\n\n, retries — falls through to the real Workflows engine untouched.\n\n## Dynamic Workers are the primitive\n\nStep back from the specifics for a second. Every interesting line of this library is either a wrapper around `.create()`\n\non the outbound side or a wrapper around `WorkflowEntrypoint`\n\non the inbound side. The actual work — spinning up the tenant's code, sandboxing it, routing RPC across the boundary, caching the isolate, hibernating between steps — is all done by Dynamic Workers underneath.\n\nThat's the real story, and it's a lot bigger than Workflows\n\nDynamic Workers is the primitive that swallows everything. __Durable Object Facets__ is the same pattern applied to Durable Objects. Dynamic Workflows is that same pattern applied to `WorkflowEntrypoint`\n\n. Each one is the same small amount of envelope-and-unwrap glue between the static binding you've always had and the dynamic version you can now hand to your customers.\n\nAnd we're not stopping at Workflows. Every binding that Workers currently exposes is heading for a dynamic counterpart — queues where each producer ships its own handler, caches, databases, object stores, AI bindings, and MCP servers where every tenant brings their own tools. Whatever you bind to a Worker today, you will soon be able to bind dynamically: dispatched per tenant, per agent, per request, at zero idle cost.\n\nThe unit economics of running a platform like this are, frankly, absurd. Shipping a multi-tenant product used to mean giving every customer their own container, their own database, their own disk, their own scheduler, and stitching it together with orchestration glue, service meshes, and hair-pulling billing math. Many of these applications have to support thousands of customers at the very least; millions, at the most. On Dynamic Workers and everything composing on top of them, idle tenants cost approximately nothing and active tenants share the same hardware through isolate-level multi-tenancy. The floor drops several orders of magnitude. A platform that used to cap out at thousands of paying customers can now reasonably serve tens of millions.\n\nCoding agents — __OpenCode__, __Claude Code__, Codex, Pi — have been proving for the past year that LLMs are far better at *writing code* than at making sequential tool calls. The __Cloudflare Agents SDK__ and __Project Think__ extend that insight into durable execution: with primitives like fibers and sub-agents, an agent's long-running plan can survive crashes, hibernation, and redeploys without the user noticing.\n\nDynamic Workflows is the piece that lets that plan be a *first-class Cloudflare Workflow* — something the agent literally writes and the platform literally runs, with the full durability machinery behind it. A `run(event, step)`\n\nfunction the model wrote a minute ago, where every `step.do(...)`\n\nis independently retryable, every `step.sleep('24 hours')`\n\nhibernates for free, and every `step.waitForEvent(...)`\n\nwaits indefinitely for the human to approve the next action. The agent writes the workflow; the platform runs it; neither has to know ahead of time what the plan looks like.\n\n### SDKs and frameworks where the user brings the logic\n\nIf you're shipping a framework where your customer writes the `run(event, step)`\n\nfunction — a workflow builder UI, a visual automation tool, a per-tenant extension system, a low-code tool for non-developers — Dynamic Workflows is now the primitive that makes it work without compromise. You call `wrapWorkflowBinding({ tenantId })`\n\nonce, hand the result to their code as `WORKFLOWS`\n\n, and every workflow instance they create is automatically tagged, routed back, and executed in their sandbox. The framework owns the Worker Loader; the user owns the workflow; neither has to care about the other.\n\nHere's the use case that's been getting us most excited.\n\nEvery CI/CD platform in existence is, underneath, a dispatcher of per-repo configuration files: *\"run these steps, in this order, with these secrets, cache these directories, upload these artifacts.\"* Each repo has its own pipeline. Each branch might have its own variant. Each pull request spawns an instance of that pipeline that has to run to completion, survive a machine crash, retry a flaky step, stream logs, pause for approvals, and persist results.\n\nThat's *exactly* the shape of a durable workflow. The reason CI hasn't been built that way until now is that nobody had a cloud primitive where **the workflow itself is different for every repo, dispatched at runtime, at zero provisioning cost.** Now you do.\n\nHere's what a CI pipeline looks like when it's just code your customer ships with their repo — say, in `.cloudflare/ci.ts`\n\n. The workflow itself is real; the `runInSandbox() / summarise()`\n\n/ GitHub binding helpers below are platform-provided glue, the kind of thing you'd ship once in your dispatcher:\n\n``` js\nimport { WorkflowEntrypoint } from 'cloudflare:workers';\n\nexport class CIPipeline extends WorkflowEntrypoint {\n  async run(event, step) {\n    const { repo, sha, branch, pr } = event.payload;\n\n    // Fork an isolated copy of the repo at this commit. Seconds, not minutes.\n    const workspace = await step.do('checkout', () =>\n      this.env.ARTIFACTS.fork(repo, { sha })\n    );\n\n    await step.do('install', () => runInSandbox(workspace, ['pnpm', 'install']));\n\n    // Each parallel step is independently retryable.\n    const [lint, test, build] = await Promise.all([\n      step.do('lint',  () => runInSandbox(workspace, ['pnpm', 'lint'])),\n      step.do('test',  () => runInSandbox(workspace, ['pnpm', 'test'])),\n      step.do('build', () => runInSandbox(workspace, ['pnpm', 'build'])),\n    ]);\n\n    if (pr) {\n      await step.do('comment', () =>\n        this.env.GITHUB.commentOnPR(repo, pr, summarise({ lint, test, build }))\n      );\n    }\n\n    // Workflow hibernates until approval arrives. No VM held open.\n    if (branch === 'main') {\n      await step.waitForEvent('approval', { type: 'deploy-approval', timeout: '24 hours' });\n      await step.do('deploy', () => runInSandbox(workspace, ['pnpm', 'deploy']));\n    }\n  }\n}\n```\n\nThe platform owns the dispatcher. It ingests a webhook, figures out which repo it came from, loads *that repo's* `CIPipeline`\n\nclass as a Dynamic Worker, and hands the run-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.\n\nNow line up what each step actually does:\n\n__Artifacts__ gives every repo a Git-native, versioned filesystem that lives on Cloudflare's globally distributed network. __ArtifactFS__ hydrates the tree lazily, so even a multi-GB repo is ready to work within single-digit seconds — and `fork()`\n\ngives each CI run its own isolated copy, with no `git clone`\n\ntax.\n\n__Dynamic Workers__ run each lightweight step (lint, format, typecheck, bundle) in a sandboxed isolate that boots in milliseconds, on the same machine as the repo's data. No VM provisioning, no image pull, no cold start.\n\n__Dynamic Workflows__ holds the whole run together. Steps are retryable and durable. The run hibernates for free while waiting on approvals. State and progress survive deploys, evictions, and crashes.\n\n__Sandboxes__ handle the heavy corners — the step that needs `docker build`\n\n, the integration suite that needs Postgres running, the Rust compile that needs 8 cores. Snapshots to R2 mean even those warm-start in a couple of seconds.\n\nA traditional CI run for a mid-sized JS repo looks something like: *allocate VM (15-30s) → pull base image (10s) → **git clone*\n\n* (10s) → **npm ci*\n\n* (30-60s) → run tests (actual work) → tear down*. Several minutes of ceremony before the first test runs, and you pay for the whole VM the whole time.\n\nThe same pipeline on this stack looks like: *edge fork of the repo (seconds) → each step boots a fresh isolate or snapshot-restored sandbox in milliseconds → runs the actual work → hibernates.* Nothing has to cold-start. Nothing has to be provisioned ahead of time. Nothing has to be kept warm. The repo doesn't move — the compute comes to it.\n\nCI has never been this fast, and the reason it hasn't is that none of these primitives have existed together in one place. Now they do.\n\n`@cloudflare/dynamic-workflows`\n\nis MIT-licensed and on npm today:\n\n```\nnpm install @cloudflare/dynamic-workflows\n```\n\nIt runs on top of Dynamic Workers, which is in open beta on the Workers Paid plan. The __repo__ includes a working example — an interactive browser playground where you write a `TenantWorkflow`\n\nclass, hit **Run**, and watch the steps execute with live-streaming logs and a per-step checklist that lights up as each `step.do()`\n\ncommits. Clone it, deploy it, show it to a coworker.\n\nIf you're a platform, an SDK, a framework, or a CI/CD product, and you want to give your customers their own workflows without running their code in your own process: this is the primitive we built for you. If you're building agents that write durable plans, this is the primitive that makes those plans *real* Workflows. If you're just watching all of this, and it looks fun to build on top of: we'd love to see what you make.\n\nFind us in the __Cloudflare Developers Discord__.", "url": "https://wpnews.pro/news/introducing-dynamic-workflows-durable-execution-that-follows-the-tenant", "canonical_source": "https://blog.cloudflare.com/dynamic-workflows/", "published_at": "2026-05-01 13:00:00+00:00", "updated_at": "2026-05-24 03:09:50.515456+00:00", "lang": "en", "topics": ["cloud-computing", "developer-tools", "products"], "entities": ["Workers", "Cloudflare", "Dynamic Workers", "Durable Object Facets", "Artifacts", "Dynamic Workflows"], "alternates": {"html": "https://wpnews.pro/news/introducing-dynamic-workflows-durable-execution-that-follows-the-tenant", "markdown": "https://wpnews.pro/news/introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.md", "text": "https://wpnews.pro/news/introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.txt", "jsonld": "https://wpnews.pro/news/introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.jsonld"}}