Durable execution has traditionally meant standing up your own orchestration layer and babysitting it. Cloudflare’s answer is to fold durability into the Workers runtime itself.

Workflows, Cloudflare’s durable execution engine for building reliable multi-step applications, has entered open beta. Any developer on a free or paid Workers plan can build and deploy one immediately — no waitlist or sign-up required.

To get started, you can scaffold and deploy from a single command:

npm create cloudflare@latest workflows-starter -- \
  --template "cloudflare/workflows-starter"

Then open src/index.ts, extend it, and push with wrangler deploy. For a fuller walkthrough, see the official guide.

What durable execution means here

Workflows is Cloudflare’s interpretation of durable execution, first announced during Developer Week. The core idea: applications should survive errors, network interruptions, upstream API outages, rate limits, and infrastructure failures without manual intervention.

With over 2.4 million developers building on Workers, R2, and Workers AI, Cloudflare observed a growing pattern: multi-step applications that process user data, transform unstructured input into structured output, export metrics, persist state along the way, and retry or restart automatically. Writing that logic by hand — making it truly durable — is notoriously difficult.

Workflows handles the hard parts for you. It manages retries, emits metrics, and durably stores state as your workflow progresses, with no database to provision. The key differentiator from other durable execution tools is that Cloudflare operates the compute and storage infrastructure underneath. You are not running a cluster, tuning autoscaling for Monday-morning spikes, or worrying about regional deployment. Your code runs on Workers; Cloudflare runs the platform.

A concrete example

Consider post-processing user file uploads that arrive in an R2 bucket via pre-signed URL. The work might include:

  • Text extraction using a Workers AI model
  • Validation calls to a third-party API
  • Database updates or queries after processing completes

Every one of those actions can fail: upstream APIs go down, rate limits kick in, databases hiccup. Writing retry logic with backoffs around each one is boilerplate. Worse, if a later step fails, naive implementations restart from scratch, repeating expensive work and risking new rate limits.

Steps: the retriable core

The fundamental unit of a Workflow is the step: an individually retriable component that can optionally emit state. That state is persisted, even if subsequent steps fail. Your application can resume without restarting and skip redundant work.

export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
	async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
		const files = await step.do('my first step', async () => {
			return {
				inputParams: event,
				files: [
					'doc_7392_rev3.pdf',
					'report_x29_final.pdf',
					'memo_2024_05_12.pdf',
					'file_089_update.pdf',
					'proj_alpha_v2.pdf',
					'data_analysis_q2.pdf',
					'notes_meeting_52.pdf',
					'summary_fy24_draft.pdf',
				],
			};
		});

		// Other steps...
	}
}

A single Workflow can contain hundreds of steps. The Rules of Workflows recommend encapsulating every API call or stateful action as its own step. Each step can define a custom retry strategy with automatic backoff, optional delay, and an eventual give-up threshold.

Worked example: embeddings pipeline

Imagine an application that reads text files from R2, chunks them, generates embeddings with Workers AI, and upserts results into Vectorize for semantic search. In the Workflows model, each phase is a discrete step.do call:

  1. Read files from storage, emit the list of filenames
  2. Chunk the text, emit results
  3. Generate text embeddings
  4. Upsert into Vectorize, capture a test query's result
await step.do(
	'make a call to write that could maybe, just might, fail',
	// Define a retry strategy
	{
		retries: {
			limit: 5,
			delay: '5 seconds',
			backoff: 'exponential',
		},
		timeout: '15 minutes',
	},
	async () => {
		// Do stuff here, with access to the state from our previous steps
		if (Math.random() > 0.5) {
			throw new Error('API call to $STORAGE_SYSTEM failed');
		}
	},
);

These steps can be further decomposed for finer-grained resilience: one step per file chunk, or one per embedding API call. Steps can be created programmatically or conditionally based on input — you need not define every step ahead of time, and each Workflow instance can create steps dynamically as it runs.

Platform Architecture: Three Layers Deep

Workflows is built entirely on the Cloudflare Developer platform. The architecture breaks down into three distinct blocks: user-facing APIs, the managed platform, and workflow instances. Users interact through REST calls to the public API gateway, Worker bindings, Wrangler, or the Dashboard UI. The managed platform layer contains the internal configuration APIs, binding shim, and account controllers, all running on Workers with SQLite-backed Durable Objects. Workflow instances are independent clones of the workflow application, each with a one-to-one relationship to a managed engine that powers it.

image9

Stateless Entry Points

The Configuration API and Binding Shim are two stateless Workers. The Configuration API receives REST calls from the API Gateway, Wrangler, or Dashboard, while the Binding Shim serves as the endpoint for the Workflows binding — an efficient, authenticated interface for Workers scripts. The Configuration API worker uses HonoJS and Zod to implement REST endpoints declared in an OpenAPI schema, which are exported to the API Gateway to add methods to the Cloudflare API catalog. These Workers share most of their code and, once authenticated, delegate operations to an Account Controller Durable Object using the account ID.

image6

Per-Account Controllers

Each Cloudflare account using Workflows gets its own Account Controller Durable Object — a dedicated persisted database storing the list of workflows, versions, and instances for that account. Scaling to millions of controllers is possible because Durable Objects with SQLite backend are single-threaded singletons bound to a stateful storage API. They run as Workers with access to all other Cloudflare APIs, making it easy to build consistent, highly available distributed applications.

Using one Durable Object per account provides several advantages:

  • Sharding aligns with internal resource management boundaries; bugs or state inconsistencies during beta are confined to the affected account.
  • Durable Object instances run close to the end user — an account in London connects through the LHR data center, while one in Lisbon connects to LIS.
  • Each account is a Worker, so gradual upgrades can start with internal users to derisk real customers.

Before SQLite, the only storage option was the Durable Object key-value API. Having a relational database enables creating tables and running complex queries. The internal method getWorkflow() demonstrates this:

async function getWorkflow(accountId: number, workflowName: string) {
  try {
    const res = this.ctx.storage.transactionSync(() => {
      const cursor = Array.from(
        this.ctx.storage.sql.exec(
          `
                    SELECT *,
                    (SELECT class_name
                        FROM   versions
                        WHERE  workflow_id = w.id
                        ORDER  BY created_on DESC
                        LIMIT  1) AS class_name
                    FROM   workflows w
                    WHERE  w.name = ? 
                    `,
          workflowName
        )
      )[0] as Workflow;

      return cursor;
    });

    this.sendAnalytics(accountId, begin, "getWorkflow");
    return res as Workflow | undefined;
  } catch (err) {
    this.sendErrorAnalytics(accountId, begin, "getWorkflow");
    throw err;
  }
}

Workflows also leverages JavaScript-native RPC when communicating between components. Previously, components had to use fetch() to make HTTP requests with serialized parameters and payloads. Now, an async call to a remote object's method works as if it were local. This is more natural, more efficient, and enables TypeScript type-checking. Calling the Account Controller's countWorkflows() from the Configuration API went from this:

const resp = await accountStub.fetch(
      "https://controller/count-workflows",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json; charset=utf-8",
        },
        body: JSON.stringify({ accountId }),
      },
    );

if (!resp.ok) {
  return new Response("Internal Server Error", { status: 500 });
}

const result = await resp.json();
const total_count = result.total_count;

to this with RPC:

const total_count = await accountStub.countWorkflows(accountId);

RPC supports passing not only Structured Cloneable objects but also entire classes — a feature that becomes critical in the Engine design.

The Engine as a Game Loop

Every workflow instance runs alongside an Engine instance, which starts the user's entry point, executes steps, handles results, and tracks state until completion. Initial prototypes modeled the Engine as a state machine, but that requires ahead-of-time understanding of userland code, implying a costly build step before running. A better model emerged: the game loop.

Games operate regardless of user input, running a sequence of tasks that implement logic and update state. An oversimplified workflow engine follows the same pattern — a loop performing the same logical tasks until the last step completes.

while (last step not completed)
    iterate every step
       use memoized cache as response if the step has run already
       continue running step or timer if it hasn't finished yet
end while

The Engine and instance have a one-to-one relationship. The Engine is managed platform code using SQLite and platform APIs internally, which Cloudflare can update with new features and bug fixes transparently. The instance is the account-owned Worker script declaring workflow steps.

When something passes a callback into step.do(), execution switches to the Engine. Because JS RPC allows passing application-defined classes extending RpcTarget, this is what happens behind the scenes (simplified):

export class Context extends RpcTarget {

  async do<T>(name: string, callback: () => Promise<T>): Promise<T> {

    // First we check we have a cache of this step.do() already
    const maybeResult = await this.#state.storage.get(name);

    // We return the cache if it exists
    if (maybeValue) { return maybeValue; }

    // Else we run the user callback
    return doWrapper(callback);
  }

}

A more complete view of the Engine's step.do() lifecycle shows how it handles all step complexities, exposing a simple API to end users even while handling logging, exceptions, and queuing:

image5

Every workflow instance is an Engine behind the scenes, and every Engine is an SQLite-backed Durable Object. This guarantees isolation between instance runtimes and states, and makes scaling to billions of instances a solved problem for Durable Objects.

How Durability Works

Workflow engines must handle long-lived processes where functions can time out, fail due to remote server errors, network issues, or need retries. Durability means that if a workflow fails, the Engine can re-run it, resume from the last recorded step, and deterministically recalculate state from cached responses of successful steps. Steps are stateful and idempotent — they produce the same result regardless of how many times they run, preventing duplicate effects like sending the same invoice twice.

image7

The technique for handling failures and retries mirrors what a step.sleep() uses for sleeping days or months: combining scheduler.wait() from the WICG Scheduling API (already supported) with Durable Object alarms, which schedule the Durable Object to wake at a future time. These APIs overcome the lack of guarantees that a Durable Object runs forever. Every state transition through userland code persists in the Engine's strongly consistent SQLite, tracking timestamps for when steps begin, their attempts if retries are needed, and their completion.

If a Durable Object gets evicted — say, during a two-month timer — pending steps are rerun on the Engine's next lifetime. The Engine's cache from the previous lifetime is hydrated, and the rerun is triggered by an alarm set to the timestamp of the next expected state transition.

A guided example: abandoned-cart emails

To see how these pieces fit together, consider a typical e-commerce problem: a customer adds items to a cart and leaves without checking out. Sending them a reminder a few days later normally requires a queue, a cron job, and a periodic database query. With Workflows, that whole pipeline collapses into a single workflow that starts when a cart is created:

import {
  WorkflowEntrypoint,
  WorkflowEvent,
  WorkflowStep,
} from "cloudflare:workers";
import { sendEmail } from "./legacy-email-provider";

type Params = {
  cartId: string;
};

type Env = {
  DB: D1Database;
};

export class Purchase extends WorkflowEntrypoint<Env, Params> {
  async run(
    event: WorkflowEvent<Params>,
    step: WorkflowStep
  ): Promise<unknown> {
    await step.sleep("wait for three days", "3 days");

    // Retrieve cart from D1
    const cart = await step.do("retrieve cart from database", async () => {
      const { results } = await this.env.DB.prepare(`SELECT * FROM cart WHERE id = ?`)
        .bind(event.payload.cartId)
        .all();
      return results[0];
    });

    if (!cart.checkedOut) {
      await step.do("send an email", async () => {
        await sendEmail("reminder", cart);
      });
    }
  }
}

This covers the happy path, but upstream email providers occasionally fail. The default retry behavior of step.do already handles transient errors, and you can tune it when your workload needs a different approach:

if (cart.isComplete) {
  await step.do(
    "send an email",
    {
      retries: {
        limit: 5,
        delay: "1 min",
        backoff: "exponential",
      },
    },
    async () => {
      await sendEmail("reminder", cart);
    }
  );
}

Managing workflows

You can interact with workflows in four ways: the REST HTTP API in Cloudflare’s API catalog, the Wrangler CLI, programmatic bindings inside a Worker, and the Cloudflare dashboard Web UI.

The HTTP API is the most flexible option. It lets any system—Cloudflare-hosted or not—trigger new workflow instances from the command line or elsewhere:

curl --request POST \
  --url https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workflows/purchase-workflow/instances/$CART_INSTANCE_ID \
  --header 'Authorization: Bearer $ACCOUNT_TOKEN \
  --header 'Content-Type: application/json' \
  --data '{
	"id": "$CART_INSTANCE_ID",
	"params": {
		"cartId": "f3bcc11b-2833-41fb-847f-1b19469139d1"
	}
  }'

Wrangler provides a more approachable set of commands with formatted output, no token handling required. Type npx wrangler workflows for help, or run:

npx wrangler workflows trigger purchase-workflow '{ "cartId": "f3bcc11b-2833-41fb-847f-1b19469139d1" }'

Workflows also has first-class support in Wrangler, including local testing. Because a workflow looks like a regular Worker with a WorkerEntrypoint, wrangler dev works without any special configuration.

❯ npx wrangler dev

 ⛅️ wrangler 3.82.0
----------------------------

Your worker has access to the following bindings:
- Workflows:
  - CART_WORKFLOW: EcommerceCartWorkflow
⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
╭───────────────────────────────────────────────╮
│  [b] open a browser, [d] open devtools        │
╰───────────────────────────────────────────────╯

Workflow APIs are exposed as a Worker binding, meaning you can run workflows programmatically from another Worker in the same account without managing permissions or authentication. This also makes it possible for workflows to call other workflows.

import { WorkerEntrypoint } from "cloudflare:workers";

type Env = { DEMO_WORKFLOW: Workflow };
export default class extends WorkerEntrypoint<Env> {
  async fetch() {
    // Pass in a user defined name for this instance
    // In this case, we use the same as the cartId
    const instance = await this.env.DEMO_WORKFLOW.create({
      id: "f3bcc11b-2833-41fb-847f-1b19469139d1",
      params: {
          cartId: "f3bcc11b-2833-41fb-847f-1b19469139d1",
      }
    });
  }
  async scheduled() {
    // Restart errored out instances in a cron
    const instance = await this.env.DEMO_WORKFLOW.get(
      "f3bcc11b-2833-41fb-847f-1b19469139d1"
    );
    const status = await instance.status();
    if (status.error) {
      await instance.restart();
    }
  }
}

Observability

Long-running, asynchronous tasks demand solid observability. You need to understand normal operation and troubleshoot when things break or you are iterating on code changes. Workflows was designed on the principle that excessive logging is a feature, not a bug.

All SQLite data for a workflow instance is accessible through the REST APIs. Here is the output for a single instance:

{
  "success": true,
  "errors": [],
  "messages": [],
  "result": {
    "status": "running",
    "params": {},
    "trigger": { "source": "api" },
    "versionId": "ae042999-39ff-4d27-bbcd-22e03c7c4d02",
    "queued": "2024-10-21 17:15:09.350",
    "start": "2024-10-21 17:15:09.350",
    "end": null,
    "success": null,
    "steps": [
      {
        "name": "send email",
        "start": "2024-10-21 17:15:09.411",
        "end": "2024-10-21 17:15:09.678",
        "attempts": [
          {
            "start": "2024-10-21 17:15:09.411",
            "end": "2024-10-21 17:15:09.678",
            "success": true,
            "error": null
          }
        ],
        "config": {
          "retries": { "limit": 5, "delay": 1000, "backoff": "constant" },
          "timeout": "15 minutes"
        },
        "output": "[email protected]",
        "success": true,
        "type": "step"
      },
      {
        "name": "sleep-1",
        "start": "2024-10-21 17:15:09.763",
        "end": "2024-10-21 17:17:09.763",
        "finished": false,
        "type": "sleep",
        "error": null
      }
    ],
    "error": null,
    "output": null
  }
}

The JSON dump includes errors, messages, current status, and per-step timestamps down to the millisecond. For a broader view, the GraphQL Analytics API provides aggregated statistics across all instances and workflows over time. This query asks for wall time data across all instances of the e-commerce-carts workflow:

{
  viewer {
    accounts(filter: { accountTag: "febf0b1a15b0ec222a614a1f9ac0f0123" }) {
      wallTime: workflowsAdaptiveGroups(
        limit: 10000
        filter: {
          datetimeHour_geq: "2024-10-20T12:00:00.000Z"
          datetimeHour_leq: "2024-10-21T12:00:00.000Z"
          workflowName: "e-commerce-carts"
        }
        orderBy: [count_DESC]
      ) {
        count
        sum {
          wallTime
        }
        dimensions {
          date: datetimeHour
        }
      }
    }
  }
}

Wrangler offers a convenience path for formatted output when describing a workflow or instance:

sid ~ npx wrangler workflows instances describe purchase-workflow latest

 ⛅️ wrangler 3.80.4

Workflow Name:         purchase-workflow
Instance Id:           d4280218-7756-41d2-bccd-8d647b82d7ce
Version Id:            0c07dbc4-aaf3-44a9-9fd0-29437ed11ff6
Status:                ✅ Completed
Trigger:               🌎 API
Queued:                14/10/2024, 16:25:17
Success:               ✅ Yes
Start:                 14/10/2024, 16:25:17
End:                   14/10/2024, 16:26:17
Duration:              1 minute
Last Successful Step:  wait for three days
Output:                false
Steps:

  Name:      wait for three days
  Type:      💤 Sleeping
  Start:     14/10/2024, 16:25:17
  End:       17/10/2024, 16:25:17
  Duration:  3 day

For dashboard-based debugging and monitoring, we have invested heavily in the Workflows UI experience.

image8

Pricing and what’s ahead

Pricing follows the Cloudflare Workers model, using CPU-based pricing rather than wall time:

image4

Workflows is thus billed only on active CPU time and requests. This matters for long-running, multi-step applications—you do not pay while a workflow sleeps, waits on an event, or makes an external network call. There are no Kubernetes clusters or VM fleets to maintain either; the infrastructure is fully handled by Cloudflare, and you pay only for consumed compute.

Today we are opening the beta program. The roadmap ahead includes triggering instances directly from queue messages and other features, fine-tuned with input from beta users. Workflows itself is built entirely on Workers and its APIs, demonstrating that anyone can build this kind of platform capability.

To connect with the team and other developers, join the #workflows-beta channel on the Cloudflare Developer Discord. Follow the Workflows changelog for beta updates, and work through the Workflows tutorial to get started.