Concepts

Durable execution

Why a workflow can pause for days and survive crashes without losing progress.

A normal function disappears if the process crashes halfway through. A durable workflow doesn't. Durablex saves the result of every step as it runs, so a workflow can pause, wait, fail, and resume - even across restarts and deploys - and never lose progress or repeat work it already finished.

Each step runs once and is remembered

When a step.run finishes, Durablex stores its result. From then on, that step is memoized: if the workflow runs again, the step returns its saved result instantly instead of executing a second time.

That's what makes retries and resumption safe. A workflow makes progress by running again and again - each time, the steps that already finished are skipped, and only the next unfinished step actually executes.

What happens when a step fails

Only the failed step retries. Everything before it is already saved, so it isn't re-run.

const charge = await ctx.step.run("charge", () => chargeCard(order));
const ship = await ctx.step.run("ship", () => createShipment(order));

If charge succeeds and ship throws, Durablex retries ship (see Retries) - charge keeps its saved result and never runs again. The card is charged exactly once.

What happens when the process restarts

Workflow progress lives in Durablex's store, not in memory. If the engine restarts - a crash, a deploy, a sleeping workflow waiting overnight - the run resumes from the last saved step. Completed steps return from memory; the workflow continues from where it stopped.

Why work must go inside a step

Because a workflow advances by running repeatedly, the code between steps runs every time. Only work inside a step.run is saved and skipped on the next pass.

So anything with a side effect or a result that can change - network calls, database reads, randomness, reading the clock - must live inside a step. Otherwise it repeats on every pass and your results drift.

Reading the clock outside a step runs on every pass, so the value changes each time:

const now = Date.now();

Inside a step it runs once and the saved value is replayed:

const now = await ctx.step.run("now", () => Date.now());

The orchestration around your steps - if branches, loops, building arguments - should be deterministic: given the same saved step results, it always reaches the same next step.

On this page