shiichan

Cloudflare Workflows now has saga rollbacks: automatic undo when a step fails!

Hello, it's Shiichan! Today's story is about a tidy-up mechanism that keeps things safe even when a step fails halfway through. Cloudflare Workflows now lets you write a rollback for when things go wrong, in the form of saga rollbacks. The engineers even walk through how they built it, so this is a great one to settle in and read!

Cloudflare Blog blog.cloudflare.com

What was announced?

On the Cloudflare Blog (an engineering post), the team introduced saga-style rollbacks for Cloudflare Workflows.

Cloudflare Workflows is a durable execution engine for multi-step applications. Each step can call external systems, retry failures, and persist state across restarts.

With this update, every step.do() can now register a compensating action to run if things fail. And because this post explains how they implemented it, there is a lot of nice detail to dig into!

The story so far

Until now, developers had to write the cleanup logic themselves whenever a step failed partway through.

The article uses a clear example: a transfer that debits Bank A, credits Bank B, and sends a confirmation email. If the credit to Bank B fails, Bank A's debit will not undo itself automatically, and the money is left stuck in limbo.

So developers had to track how far things had succeeded with try-catch blocks, manage the order of the reversals by hand, and assemble all of it outside the step definitions. As the article puts it, they had to implement their own compensation logic outside of the steps' direct definitions.

What changes

Now you pass a rollback function along with your step.do(). If the workflow ultimately fails, Workflows automatically calls the compensating actions you registered.

The nice part is that rollback handlers run through the same machinery as regular steps. Retries, timeouts, and logs all apply! Instead of living as invisible wiring outside your app code, the compensation logic now sits right next to the step definition.

Let's dive deeper!

Let's look at the mechanics a little more closely.

The API shape

The rollback function is passed as an option to step.do().

await step.do(
  "debit-bank-a",
  () => bankA.debit(from, amount),
  {
    rollback: async ({ output }) =>
      bankA.credit(from, amount, output.id),
  }
);

The rollback handler receives the error that triggered the rollback, the step context, and the step's output. If the step failed before persisting its result, output is undefined.

Which steps are eligible?

Any started or completed step that has a rollback handler is eligible. The failing step itself is eligible too, as long as it registered a handler. Handlers run in reverse step-start order, not in completion order.

When does it trigger?

Rollback only runs when the workflow itself fails terminally. If your code catches the error and continues, rollback does not run.

Idempotency matters

Rollback functions should be idempotent, just like regular steps. Use idempotency keys from your payment provider, or otherwise make the operations safe to call more than once. The article says it directly:

Rollback functions should be idempotent, just like regular Workflow steps.

Dedicated configuration

Retries and timeouts specific to rollback live in rollbackConfig.

{
  rollback: async ({ output }) => { /* ... */ },
  rollbackConfig: {
    retries: { limit: 10, delay: "30 seconds", backoff: "exponential" },
    timeout: "2 minutes",
  },
}

Under the hood

Workflows records whether each step registered compensation logic in its durable step history. During rollback, it uses those persisted records to determine what happened. Even if the engine restarts, replay mode lets it rebuild the handlers without re-running the forward steps. Clever!

What if rollback fails?

If a rollback handler exhausts its retry limit and still fails, Workflows records that rollback as failed, stops the remaining handlers, and the workflow ends in an Errored state.

What's coming next

The roadmap includes rollback support for waitForEvent, parallel rollback execution, and support for Python Workflows.

Wrap-up

  • Cloudflare Workflows now supports saga rollbacks, letting you write a compensating action per step
  • Just pass a rollback function to step.do(), and Workflows undoes completed steps when the workflow fails
  • Handlers run in reverse step-start order; eligible steps are started or completed ones that have a handler
  • Rollbacks get the same retries, timeouts, and logs as regular steps, so keep the functions idempotent
  • If a rollback fails, the workflow ends in Errored; parallel execution, waitForEvent, and Python support are planned

If you run multi-step processes that handle money, inventory, or anything else where a mid-way failure hurts, this is a very reassuring update!