
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Fri, 08 May 2026 04:27:41 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Introducing Dynamic Workflows: durable execution that follows the tenant]]></title>
            <link>https://blog.cloudflare.com/dynamic-workflows/</link>
            <pubDate>Fri, 01 May 2026 13:00:00 GMT</pubDate>
            <description><![CDATA[ Dynamic Workflows is a library that lets you route durable execution to tenant-provided code on the fly. Built on Dynamic Workers, it enables platforms to serve millions of unique workflows at near-zero idle cost. ]]></description>
            <content:encoded><![CDATA[ <p>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 <i>their</i> customers to ship code to <i>us </i>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.</p><p>Last month, when we shipped the <a href="https://blog.cloudflare.com/dynamic-workers/"><u>Dynamic Workers open beta</u></a>, we gave those platforms a clean primitive for the <i>compute</i> side: hand the Workers runtime some code at runtime, get back an isolated, sandboxed Worker, on the same machine, in single-digit milliseconds. <a href="https://blog.cloudflare.com/durable-object-facets-dynamic-workers/"><u>Durable Object Facets</u></a> extended the same idea to <i>storage</i> — each dynamically-loaded app can have its own SQLite database, spun up on demand, with the platform sitting in front, as a supervisor. <a href="https://blog.cloudflare.com/artifacts-git-for-agents-beta/"><u>Artifacts</u></a> did the same for <i>source control</i>: 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?</p><p>Today, we are bridging durable execution and dynamic deployment with <a href="https://github.com/cloudflare/dynamic-workflows"><b><u>Dynamic Workflows</u></b></a>.</p>
    <div>
      <h2>The gap between durable and dynamic execution</h2>
      <a href="#the-gap-between-durable-and-dynamic-execution">
        
      </a>
    </div>
    <p><a href="https://developers.cloudflare.com/workflows/"><u>Cloudflare Workflows</u></a> is our durable execution engine. It turns a <code>run(event, step)</code> function 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 <a href="https://blog.cloudflare.com/workflows-v2/"><u>Workflows V2</u></a> — up to 50,000 concurrent instances and 300 new instances per second per account, redesigned for the agentic era.</p><p>But Workflows has always had one assumption baked in: the workflow code is part of your deployment. Your <code>wrangler.jsonc</code> has a block that says <i>"when the engine calls into </i><code><i>WORKFLOWS</i></code><i>, run the class called </i><code><i>MyWorkflow</i></code><i>."</i> One binding, one class. Per deploy.</p><p>That works fine if you own all the code. It's fine if you're running a traditional application.</p><p>It stops working the moment you want to let your customer ship <i>their</i> workflow.</p><p>Say 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.</p><p>This 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.</p>
    <div>
      <h2>Dynamic Workflows</h2>
      <a href="#dynamic-workflows">
        
      </a>
    </div>
    <p><code>@cloudflare/dynamic-workflows</code> is a small library. Roughly 300 lines of TypeScript. It lets a single Worker — the <b>Worker Loader</b> — route every <code>create()</code> call to a different tenant's code, and, critically, have the Workflows engine dispatch <code>run(event, step)</code> back to that same code when the workflow actually executes, seconds or hours or days later.</p><p>Here's the whole pattern. A Worker Loader:</p>
            <pre><code>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 () =&gt; ({
    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 }) },
  }));
}

// Register this as class_name in wrangler.jsonc.
export const DynamicWorkflow = createDynamicWorkflowEntrypoint&lt;Env&gt;(
  async ({ env, metadata }) =&gt; {
    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);
  },
};</code></pre>
            <p>Add to your <code>wrangler.jsonc</code>:</p>
            <pre><code>"workflows": [
		{
			"name": "dynamic-workflow",
			"binding": "WORKFLOW",
			"class_name": "DynamicWorkflow"
		}
	]</code></pre>
            <p>The tenant writes plain, idiomatic Workflows code. They have no idea they're being dispatched:</p>
            <pre><code>import { WorkflowEntrypoint } from 'cloudflare:workers';

export class TenantWorkflow extends WorkflowEntrypoint {
  async run(event, step) {
    return step.do('greet', async () =&gt; `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 });
  },
};</code></pre>
            <p>That's it. The tenant calls <code>env.WORKFLOWS.create(...)</code> against what looks like a perfectly normal Workflow binding. Workflow IDs, <code>.status()</code>, <code>.pause()</code>, retries, hibernation, durable steps, <code>step.sleep('24 hours')</code>, <code>step.waitForEvent()</code> — everything works the way it always has.</p><p>The library handles one thing: making sure that when the Workflows engine eventually wakes up and calls <code>run(event, step)</code>, it ends up inside the <i>right tenant's</i> code.</p>
    <div>
      <h2>How it works</h2>
      <a href="#how-it-works">
        
      </a>
    </div>
    <p>Three layers: the Workflows engine (platform) on top, your Worker Loader in the middle, your tenant's code (a Dynamic Worker) on the bottom. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3D8fGfZalW4N4h7QngR8tN/59ef281c194cfcba12ea6cfb6ec240d7/image2.png" />
          </figure><p>When 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.</p><p>Walking the flow:</p><p><b>① → ② Entering the tenant's code.</b> 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 <code>default.fetch</code>. The <code>env</code> it hands the tenant contains <code>WORKFLOWS: wrapWorkflowBinding({ tenantId })</code>. As far as the tenant is concerned, that looks and acts like a real Workflow binding.</p><p><b>③ Up to the Worker Loader.</b> When the tenant calls <code>env.WORKFLOWS.create({ params })</code>, it's actually making a Remote Procedure Call (RPC) into the Worker Loader — the wrapped binding is a <code>WorkerEntrypoint</code> subclass (<code>DynamicWorkflowBinding</code>) that the runtime specialized with the tenant's metadata at load time. That's why you have to <code>export { DynamicWorkflowBinding }</code> from your Worker Loader: the runtime builds per-tenant stubs by looking the class up in <code>cloudflare:workers</code> exports. Bindings that cross the Dynamic Worker boundary <i>have</i> to be RPC stubs — a plain <code>{ create, get }</code> object can't be structured-cloned, and the raw <code>Workflow</code> binding isn't serializable either.</p><p>Inside the Worker Loader, the wrapped binding transparently rewrites the payload:</p>
            <pre><code>tenant calls:  create({ params: { name: 'Alice' } })
                            │
                            ▼
engine sees:   create({ params: {
                  __workerLoaderMetadata: { tenantId: 't-42' },
                  params: { name: 'Alice' }
               }})
</code></pre>
            <p><b>④ Up to the engine.</b> The Worker Loader then calls <code>.create()</code> on the <i>real</i> <code>WORKFLOWS</code> binding with the envelope as the params. From here the Workflows engine takes over. It persists <code>event.payload</code> — 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.</p><p>One implication: treat the metadata as a routing hint, not as authorization. The tenant can read it back via <code>instance.status()</code>. Don't put secrets in there.</p><p><b>⑤ → ⑥ The engine comes back down.</b> When the engine is ready to run a step, it calls <code>.run(event, step)</code> on the class you registered in <code>wrangler.jsonc</code> — the one <code>createDynamicWorkflowEntrypoint</code> gave you. That class unwraps the envelope, hands the metadata to the <code>loadRunner</code> callback <i>you</i> wrote, and forwards the unwrapped event through to whatever runner the callback returns.</p><p>The 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 <a href="https://www.npmjs.com/package/@cloudflare/worker-bundler"><code><u>@cloudflare/worker-bundler</u></code></a>. In the common case, you just hand off to the Worker Loader:</p>
            <pre><code>const stub = env.LOADER.get(tenantId, () =&gt; loadTenantCode(tenantId));
return stub.getEntrypoint('TenantWorkflow');</code></pre>
            <p>
The 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 <code>step.do()</code> pulls 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.</p>
    <div>
      <h3>The escape hatch</h3>
      <a href="#the-escape-hatch">
        
      </a>
    </div>
    <p>If you want to subclass <code>WorkflowEntrypoint</code> yourself — to add logging around <code>run()</code>, wire up per-tenant observability, or thread custom state through — the library exposes the lower-level <code>dispatchWorkflow</code> primitive that <code>createDynamicWorkflowEntrypoint</code> is built on:</p>
            <pre><code>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 }) =&gt; loadRunnerForTenant(env, metadata),
    );
  }
}
</code></pre>
            <p>Everything else — IDs, pause/resume, <code>sendEvent</code>, retries — falls through to the real Workflows engine untouched.</p>
    <div>
      <h2>Dynamic Workers are the primitive</h2>
      <a href="#dynamic-workers-are-the-primitive">
        
      </a>
    </div>
    <p>Step back from the specifics for a second. Every interesting line of this library is either a wrapper around <code>.create()</code> on the outbound side or a wrapper around <code>WorkflowEntrypoint</code> on 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.</p><p>That's the real story, and it's a lot bigger than Workflows</p><p>Dynamic Workers is the primitive that swallows everything. <a href="https://blog.cloudflare.com/durable-object-facets-dynamic-workers/"><u>Durable Object Facets</u></a> is the same pattern applied to Durable Objects. Dynamic Workflows is that same pattern applied to <code>WorkflowEntrypoint</code>. 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.</p><p>And 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.</p><p>The 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.</p>
    <div>
      <h2>What this unlocks</h2>
      <a href="#what-this-unlocks">
        
      </a>
    </div>
    
    <div>
      <h3>Agent platforms that plan like engineers</h3>
      <a href="#agent-platforms-that-plan-like-engineers">
        
      </a>
    </div>
    <p>Coding agents — <a href="https://opencode.ai"><u>OpenCode</u></a>, <a href="https://code.claude.com/docs/en/overview"><u>Claude Code</u></a>, Codex, Pi — have been proving for the past year that LLMs are far better at <i>writing code</i> than at making sequential tool calls. The <a href="https://developers.cloudflare.com/agents/"><u>Cloudflare Agents SDK</u></a> and <a href="https://blog.cloudflare.com/project-think"><u>Project Think</u></a> 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.</p><p>Dynamic Workflows is the piece that lets that plan be a <i>first-class Cloudflare Workflow</i> — something the agent literally writes and the platform literally runs, with the full durability machinery behind it. A <code>run(event, step)</code> function the model wrote a minute ago, where every <code>step.do(...)</code> is independently retryable, every <code>step.sleep('24 hours')</code> hibernates for free, and every <code>step.waitForEvent(...)</code> waits 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.</p>
    <div>
      <h3>SDKs and frameworks where the user brings the logic</h3>
      <a href="#sdks-and-frameworks-where-the-user-brings-the-logic">
        
      </a>
    </div>
    <p>If you're shipping a framework where your customer writes the <code>run(event, step)</code> function — 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 <code>wrapWorkflowBinding({ tenantId })</code> once, hand the result to their code as <code>WORKFLOWS</code>, 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.</p>
    <div>
      <h3>CI/CD at primitive speed</h3>
      <a href="#ci-cd-at-primitive-speed">
        
      </a>
    </div>
    <p>Here's the use case that's been getting us most excited.</p><p>Every CI/CD platform in existence is, underneath, a dispatcher of per-repo configuration files: <i>"run these steps, in this order, with these secrets, cache these directories, upload these artifacts."</i> 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.</p><p>That's <i>exactly</i> 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 <b>the workflow itself is different for every repo, dispatched at runtime, at zero provisioning cost.</b> Now you do.</p><p>Here's what a CI pipeline looks like when it's just code your customer ships with their repo — say, in <code>.cloudflare/ci.ts</code>. The workflow itself is real; the <code>runInSandbox() / summarise()</code> / GitHub binding helpers below are platform-provided glue, the kind of thing you'd ship once in your dispatcher:</p>
            <pre><code>import { WorkflowEntrypoint } from 'cloudflare:workers';

export class CIPipeline extends WorkflowEntrypoint {
  async run(event, step) {
    const { repo, sha, branch, pr } = event.payload;

    // Fork an isolated copy of the repo at this commit. Seconds, not minutes.
    const workspace = await step.do('checkout', () =&gt;
      this.env.ARTIFACTS.fork(repo, { sha })
    );

    await step.do('install', () =&gt; runInSandbox(workspace, ['pnpm', 'install']));

    // Each parallel step is independently retryable.
    const [lint, test, build] = await Promise.all([
      step.do('lint',  () =&gt; runInSandbox(workspace, ['pnpm', 'lint'])),
      step.do('test',  () =&gt; runInSandbox(workspace, ['pnpm', 'test'])),
      step.do('build', () =&gt; runInSandbox(workspace, ['pnpm', 'build'])),
    ]);

    if (pr) {
      await step.do('comment', () =&gt;
        this.env.GITHUB.commentOnPR(repo, pr, summarise({ lint, test, build }))
      );
    }

    // Workflow hibernates until approval arrives. No VM held open.
    if (branch === 'main') {
      await step.waitForEvent('approval', { type: 'deploy-approval', timeout: '24 hours' });
      await step.do('deploy', () =&gt; runInSandbox(workspace, ['pnpm', 'deploy']));
    }
  }
}</code></pre>
            <p>The platform owns the dispatcher. It ingests a webhook, figures out which repo it came from, loads <i>that repo's</i> <code>CIPipeline</code> class 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.</p><p>Now line up what each step actually does:</p><ul><li><p><a href="https://blog.cloudflare.com/artifacts-git-for-agents-beta/"><b><u>Artifacts</u></b></a> gives every repo a Git-native, versioned filesystem that lives on Cloudflare's globally distributed network. <a href="https://github.com/cloudflare/artifact-fs"><u>ArtifactFS</u></a> hydrates the tree lazily, so even a multi-GB repo is ready to work within single-digit seconds — and <code>fork()</code> gives each CI run its own isolated copy, with no <code>git clone</code> tax.</p></li><li><p><a href="https://blog.cloudflare.com/dynamic-workers/"><b><u>Dynamic Workers</u></b></a> 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.</p></li><li><p><a href="https://developers.cloudflare.com/dynamic-workers/usage/dynamic-workflows/"><b><u>Dynamic Workflows</u></b></a> 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.</p></li><li><p><a href="https://blog.cloudflare.com/sandbox-ga/"><b><u>Sandboxes</u></b></a> handle the heavy corners — the step that needs <code>docker build</code>, 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.</p></li></ul><p>A traditional CI run for a mid-sized JS repo looks something like: <i>allocate VM (15-30s) → pull base image (10s) → </i><code><i>git clone</i></code><i> (10s) → </i><code><i>npm ci</i></code><i> (30-60s) → run tests (actual work) → tear down</i>. Several minutes of ceremony before the first test runs, and you pay for the whole VM the whole time.</p><p>The same pipeline on this stack looks like: <i>edge fork of the repo (seconds) → each step boots a fresh isolate or snapshot-restored sandbox in milliseconds → runs the actual work → hibernates.</i> 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.</p><p>CI 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.</p>
    <div>
      <h2>Try it</h2>
      <a href="#try-it">
        
      </a>
    </div>
    <p><code>@cloudflare/dynamic-workflows</code> is MIT-licensed and on npm today:</p>
            <pre><code>npm install @cloudflare/dynamic-workflows</code></pre>
            <p>It runs on top of Dynamic Workers, which is in open beta on the Workers Paid plan. The <a href="https://github.com/cloudflare/dynamic-workflows"><u>repo</u></a> includes a working example — an interactive browser playground where you write a <code>TenantWorkflow</code> class, hit <b>Run</b>, and watch the steps execute with live-streaming logs and a per-step checklist that lights up as each <code>step.do()</code> commits. Clone it, deploy it, show it to a coworker.</p><p>If 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 <i>real</i> 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.</p><p>Find us in the <a href="https://discord.cloudflare.com/"><u>Cloudflare Developers Discord</u></a>.</p> ]]></content:encoded>
            <category><![CDATA[Workflows]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Durable Execution]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Developers]]></category>
            <guid isPermaLink="false">4JZIrwtIvEd4qwAd4JAThB</guid>
            <dc:creator>Dan Lapid</dc:creator>
            <dc:creator>Luís Duarte</dc:creator>
        </item>
        <item>
            <title><![CDATA[Rearchitecting the Workflows control plane for the agentic era]]></title>
            <link>https://blog.cloudflare.com/workflows-v2/</link>
            <pubDate>Wed, 15 Apr 2026 13:00:00 GMT</pubDate>
            <description><![CDATA[ Cloudflare Workflows, a durable execution engine for multi-step applications, now supports higher concurrency and creation rate limits through a rearchitectured control plane, helping scale to meet the use cases for durable background agents.
 ]]></description>
            <content:encoded><![CDATA[ <p>When we originally built <a href="https://developers.cloudflare.com/workflows/"><u>Workflows</u></a>, our durable execution engine for multi-step applications, it was designed for a world in which workflows were triggered by human actions, like a user signing up or placing an order. For use cases like onboarding flows, workflows only had to support one instance per person — and people can only click so fast. </p><p>Over time, what we’ve actually seen is a quantitative shift in the workload and access pattern: fewer human-triggered workflows, and more agent-triggered workflows, created at machine speed. </p><p>As agents become persistent and autonomous infrastructure, operating on behalf of users for hours or days, they need a durable, asynchronous execution engine for the work they are doing. Workflows provides exactly that: every step is independently retryable, the workflow can pause for human-in-the-loop approval, and each instance survives failures without losing progress.  </p><p>Moreover, workflows themselves are being used to implement agent loops and serve as the durable harnesses that manage and keep agents alive. Our<a href="https://developers.cloudflare.com/changelog/post/2026-02-03-agents-workflows-integration/"> <u>Agents SDK integration</u></a> accelerated this, making it easy for agents to spawn workflow instances and get real-time progress back. A single agent session can now kick off dozens of workflows, and many agents running concurrently means thousands of instances created in seconds. With <a href="https://blog.cloudflare.com/project-think"><u>Project Think</u></a> now available, we anticipate that velocity will only increase.</p><p>To help developers scale their agents and applications on Workflows, we are excited to announce that we now support:</p><ul><li><p>50,000 concurrent instances (number of workflow executions running in parallel), <a href="https://developers.cloudflare.com/changelog/post/2025-02-25-workflows-concurrency-increased/"><u>originally 4,500</u></a></p></li><li><p>300 instances/second created per account, previously 100</p></li><li><p>2 million queued instances (meaning instances that have been created or awoken and are waiting for a concurrency slot) per workflow, up from 1 million</p></li></ul><p>We redesigned the Workflows control plane from usage data and first principles to support these increases. For V1 of the control plane, a single Durable Object (DO) could serve as the central registry and coordinator of an entire account. For V2, we built two new components to help horizontally scale the system and alleviate the bottlenecks that V1 introduced, before migrating all customers — with live traffic — seamlessly onto the new version.</p>
    <div>
      <h2>V1: initial architecture of Workflows</h2>
      <a href="#v1-initial-architecture-of-workflows">
        
      </a>
    </div>
    <p>As described in our <a href="https://blog.cloudflare.com/building-workflows-durable-execution-on-workers/#building-cloudflare-on-cloudflare"><u>public beta blog post</u></a>, we built <a href="https://www.cloudflare.com/developer-platform/products/workflows/"><u>Workflows</u></a> entirely on our own developer platform. Fundamentally, a workflow is a series of durable steps, each independently retryable, that can execute tasks, wait for external events, or sleep until a predetermined time. </p>
            <pre><code>export class MyWorkflow extends WorkflowEntrypoint {

  async run(event, step) {
    const data = await step.do("fetch-data", async () =&gt; {
      return fetchFromAPI();
    });

    const approval = await step.waitForEvent("approval", {
      type: "approval",
      timeout: "24 hours",
    });

    await step.do("process-and-save", async () =&gt; {
      return store(transform(data));
    });
  }
}
</code></pre>
            <p>To trigger each instance, execute its logic, and store its metadata, we leverage SQLite-backed <a href="https://www.cloudflare.com/developer-platform/products/durable-objects/"><u>Durable Objects</u></a>, which are a simple but powerful primitive for coordination and storage within a distributed system. </p><p>In the control plane, some Durable Objects — like the <i>Engine</i>, which executes the actual workflow instance, including its step, retry, and sleep logic — are spun up at a ratio of 1:1 per instance. On the other hand, the <i>Account</i> is an account-level Durable Object that manages all workflows and workflow instances for that account.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/55bqaUjc30HJHe9spWYTo8/d8053955660553db8b64a484fb321ec7/BLOG-3116_2.png" />
          </figure><p>To learn more about the V1 control plane, refer to our <a href="https://blog.cloudflare.com/building-workflows-durable-execution-on-workers/"><u>Workflows announcement blog post</u></a>.</p><p>After we launched Workflows into beta, we were thrilled to see customers quickly scaling their use of the product, but we also realized that having a single Durable Object to store all that account-level information introduced a bottleneck. Many customers needed to create and execute hundreds or even thousands of Workflow instances per minute, which could quickly overwhelm the <i>Account</i> in our original architecture. The original rate limits — 4,500 concurrency slots and 100 instance creations per 10 seconds — were a result of this limitation. </p><p>On the V1 control plane, these limits were a hard cap. Any and all operations depending on <i>Account</i>, including create, update, and list, had to go through that single DO. Users with high concurrency workloads could have thousands of instances starting and ending at any given moment, building up to thousands of requests per second to <i>Account</i>. To solve for this, we rearchitected the workflow control plane such that it horizontally scales to higher concurrency and creation rate limits. </p>
    <div>
      <h2>V2: horizontal scale for higher throughput</h2>
      <a href="#v2-horizontal-scale-for-higher-throughput">
        
      </a>
    </div>
    <p>For the new version, we rethought every single operation from the ground up with the goal of optimizing for high-volume workflows. Ultimately, Workflows should scale to support whatever developers need – whether that is thousands of instances created per second or millions of instances running at a time. We also wanted to ensure that V2 allowed for flexible limits, which we can toggle and continue increasing, rather than the hard cap which V1 limits imposed. After many design iterations, we settled on the following pillars for our new architecture: </p><ul><li><p>The source of truth for the existence of a given instance should be its <i>Engine</i> and nothing else. </p><ul><li><p>In the V1 control plane architecture, we lacked a check before queuing the instance as to whether its <i>Engine</i> actually existed. This allowed for a bad state where an instance may have been queued without its corresponding <i>Engine </i>having spun up. </p></li><li><p>Instance lifecycle and liveness mechanisms must be horizontally scalable per-workflow and distributed throughout many regions.</p></li></ul></li><li><p>The new Account singleton should only store the minimum necessary metadata and have an invariant maximum amount of concurrent requests.</p></li></ul>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1txhhObwwIcV8C2gr9Hjfe/df7ea739567c7e42471458357c16583d/unnamed.png" />
          </figure><p>There are two new, critical components in the V2 control plane which allowed us to improve the scalability of Workflows: <i>SousChef</i> and <i>Gatekeeper</i>. The first component, <i>SousChef</i>, is a “second in command” to the <i>Account</i>. Recall that previously, the <i>Account</i> managed the metadata and lifecycle for all of the instances across all of the workflows within a given account. <i>SousChef</i> was introduced to keep track of metadata and lifecycle on a <b>subset</b> of instances in a given workflow. Within an account, a distribution of <i>SousChefs</i> can then report back to <i>Account</i> in a more efficient and manageable way. (An added benefit of this design: not only did we already have per-account isolation, but we also inadvertently gained “per-workflow” isolation within the same account, since each <i>SousChef</i> only takes care of one specific workflow).</p><p>The second component, <i>Gatekeeper</i>, is a mechanism to distribute concurrency “slots” (derived from concurrency limits) across all <i>SousChefs</i> within the account. It acts as a leasing system. When an instance is created, it is randomly assigned to one of the <i>SousChefs</i> within that account. Then the <i>SousChef</i> makes a request to <i>Account</i> to trigger that instance. Either a slot is granted, or the instance is queued. Once the slot is granted, the <i>SousChef</i> triggers execution of the instance and assumes responsibility that the instance never gets stuck. </p><p><i>Gatekeeper</i> was needed to make sure that <i>Engines</i> never overloaded their <i>Account</i> (a pressing risk on V1) so every communication between <i>SousChefs</i> and their <i>Account</i> happens on a periodic cycle, once per second — each cycle will also batch all slot requests, ensuring that only one JSRPC call is made. This ensures the instance creation rate can never overload or influence the most important component, <i>Account</i> (as an aside: if the <i>SousChef </i>count is too high, we rate-limit calls or spread across different <i>SousChefs</i> throughout different time periods). Also, this periodic property allows us to preserve fairness on older instances and to ensure max-min fairness through the many <i>SousChefs</i>, allowing them all to progress. For example, if an instance wakes up, it should be prioritized for a slot over a newly created instance, but each <i>SousChef</i> ensures that its own instances do not get stuck.</p><p>This architecture is more distributed, and therefore, more scalable. Now, when an instance is created, the request path is:</p><ol><li><p>Check control plane version</p></li><li><p>Check if a cached version of the workflow and version details is available in that location</p><ol><li><p>If not, check <i>Account</i> to get workflow name, unique ID, and version, and cache that information</p></li></ol></li><li><p>Store only necessary metadata (instance payload, creation date) onto its own <i>Engine</i></p></li></ol><p>So, how does <i>Engine</i> tell the control plane that it now exists? That happens in the background after instance metadata is set. As background operations on a Durable Object can fail, due to eviction or server failure, we also set an “alarm” on <i>Engine</i> in the creation hot-path. That way, if the background task does not finish, the alarm <b>ensures</b> that the instance will begin. </p><p>A <a href="https://developers.cloudflare.com/durable-objects/api/alarms/"><u>Durable Object alarm</u></a> allows a Durable Object instance to be awakened at a fine-grained time in the future with an<b> at-least-once </b>execution model, with automatic retries built in. We extensively use this combination of background “tasks” and alarms to remove operations off the hot-path while still ensuring that everything will happen as planned. That’s how we keep critical operations like <i>creating an instance</i> fast without ever compromising on reliability. </p><p>Other than unlocking scale, this version of the control plane means that: </p><ul><li><p>Instance listing performance is faster, and actually consistent with cursor pagination; </p></li><li><p>Any operation on an instance does exactly one network hop (as it can go directly to its <i>Engine</i>, ensuring that eyeball request latency is as small as we can manage);</p></li><li><p>We can ensure that more instances are actually behaving correctly (by running on time) concurrently (and correct them if not, making sure that <i>Engines</i> are never late to continue execution).</p></li></ul>
    <div>
      <h2>V1 → V2 migration</h2>
      <a href="#v1-v2-migration">
        
      </a>
    </div>
    <p>Now that we had a new version of the Workflows control plane that can handle a higher volume of user load, we needed to do the “boring” part: migrating our customers and instances to the new system. At Cloudflare’s scale, this becomes a problem in and of itself, so the “boring” part becomes the biggest challenge. Well before its one-year mark, Workflows had already racked up millions of instances and thousands of customers. Also, some tech debt on V1’s control plane meant that a queued instance might not have its own <i>Engine</i> Durable Object created yet, complicating matters further.</p><p>Such a migration is tricky because customers might have instances running at any given moment; we needed a way to add the <i>SousChef</i> and <i>Gatekeeper</i> components into older accounts without causing any disruption or downtime.</p><p>We ultimately decided that we would migrate existing <i>Accounts </i>(which we’ll refer to as <i>AccountOlds) </i>to behave like <i>SousChefs. </i>By persisting the <i>Account</i> DOs, we maintained the instance metadata, and simply converted the DO into a <i>SousChef</i> “DO”: </p>
            <pre><code>// You might be wondering what's this SousChef class? This is the SousChef DO class!
import { SousChef } from "@repo/souschef";

class AccountOld extends DurableObject {
  constructor(state: DurableObjectState, env: Env) {
    // We added the following snippet to the end of our AccountOld DO's
    // constructor. This ensures that if we want, we can use any primitive
    // that is available on SousChef DO
    if (this.currentVersion === ControlPlaneVersions.SOUS_CHEFS) {
      this.sousChef = new SousChef(this.ctx, this.env);
      await this.sousChef.setup()
    }
  }

  async updateInstance(params: UpdateInstanceParams) {
    if (this.currentVersion === ControlPlaneVersions.SOUS_CHEFS) {
      assert(this.sousChef !== undefined, 'SousChef must exist on v2');
      return this.sousChef.updateInstance(params);
    }

    // old logic remains the same
  }

  @RequiresVersion&lt;AccountOld&gt;(ControlPlaneVersions.V1)
  async getMetadata() {
    // this method can only be run if 
    // this.currentVersion === ControlPlaneVersions.V1
  }
}</code></pre>
            <p>We can instantiate the <i>SousChef</i> class within the <i>AccountOld</i> because the SQL tables that track instance metadata, on both <i>SousChefs</i> and <i>AccountOld</i> DOs, are the same on both. As such, we could just decide which version of the code to use. If this hadn’t been the case, we would have been forced to migrate the metadata of millions of instances, which would have made the migration more difficult and longer running for each account. So, how did the migration work?</p><p>First, we prepared <i>AccountOld</i> DOs to be switched to behave as <i>SousChefs</i> (which meant creating a release with a version of the snippet above). Then, we enabled control plane V2 per account, which triggered the next three steps roughly at the same time:</p><ul><li><p>All new instance creation requests are now routed to the new <i>SousChefs</i> (<i>SousChefs</i> are created when they receive the first request), new instances never go to <i>AccountOld</i> again;</p></li><li><p><i>AccountOld</i> DOs start migrating themselves to behave like <i>SousChefs</i>;</p></li><li><p>The new <i>Account</i> DO is spun up with the corresponding metadata.</p></li></ul><p>After all accounts were migrated to the new control plane version, we were able to sunset <i>AccountOld</i> DOs as their instance retention periods expired. Once all instances on all accounts on <i>AccountOlds</i> were migrated, we could spin down those DOs permanently. The migration was completed with no downtime in a process that truly felt like changing a car’s wheels while driving.</p>
    <div>
      <h2>Try it out</h2>
      <a href="#try-it-out">
        
      </a>
    </div>
    <p>If you are new to Workflows, try our <a href="https://developers.cloudflare.com/workflows/get-started/guide/"><u>Get Started guide</u></a> or <a href="https://developers.cloudflare.com/workflows/get-started/durable-agents/"><u>build your first durable agent</u></a> with Workflows.</p><p>If your use case requires higher limits than our new defaults — a concurrency limit of 50,000 slots and account-level creation rate limit of 300 instances per second, 100 per workflow — reach out via your account team or the <a href="https://forms.gle/ukpeZVLWLnKeixDu7"><u>Workers Limit Request Form</u></a>. You can also reach out with feedback, feature requests, or just to share how you are using Workflows on our <a href="https://discord.com/channels/595317990191398933/1296923707792560189"><u>Discord server</u></a>.</p> ]]></content:encoded>
            <category><![CDATA[Agents Week]]></category>
            <category><![CDATA[Agents]]></category>
            <category><![CDATA[Durable Objects]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Developers]]></category>
            <guid isPermaLink="false">5R3ZpKlSDaSxbIwmpXwWYJ</guid>
            <dc:creator>Luís Duarte</dc:creator>
            <dc:creator>Mia Malden</dc:creator>
            <dc:creator>André Venceslau</dc:creator>
        </item>
    </channel>
</rss>