Durable execution
Why a long agent picks up where it stopped instead of starting over: each step's result is recorded the moment it completes, and replay skips it.
A workflow makes progress by running its handler again and again. Duraton records the result of every step the moment it completes, so each pass replays the finished steps from their recorded results and executes only the next unfinished one.
const charge = await ctx.step.run("charge", () => chargeCard(order));
await ctx.step.sleep("settle", "1h");
const ship = await ctx.step.run("ship", () => createShipment(charge));charge = await ctx.step.run("charge", lambda: charge_card(order))
await ctx.step.sleep("settle", "1h")
ship = await ctx.step.run("ship", lambda: create_shipment(charge))charge, err := duraton.Run(c, "charge", func() (Charge, error) {
return chargeCard(order)
})
if err != nil {
return nil, err
}
if err := duraton.Sleep(c, "settle", time.Hour); err != nil {
return nil, err
}
ship, err := duraton.Run(c, "ship", func() (Shipment, error) {
return createShipment(charge)
})Interrupt this run after charge - a crash, a deploy, an hour of sleep - and the next pass replays
charge from its recorded result without calling chargeCard again, then continues at ship. The
card is charged exactly once.
When a step fails
Only the failed step retries. Every step before it already has a recorded result, so it is not
re-executed. If charge succeeds and ship throws, Duraton retries ship alone (see
Retries).
When work must go inside a step
The code between steps runs on every pass. Only work inside a step is recorded and replayed.
Anything with a side effect, or a result that can change between passes - network calls, database reads, randomness, reading the clock - must live inside a step, or it repeats on every pass and its value drifts:
const drifts = Date.now(); // re-read on every pass
const stable = await ctx.step.run("now", () => Date.now()); // recorded once, replayed afterdrifts = time.time() # re-read on every pass
stable = await ctx.step.run("now", lambda: time.time()) # recorded once, replayed afterdrifts := time.Now() // re-read on every pass
stable, err := duraton.Run(c, "now", func() (time.Time, error) { // recorded once, replayed after
return time.Now(), nil
})The code between steps must be deterministic
Given the same recorded step results, the orchestration around your steps - if branches, loops,
building arguments, choosing step ids - must reach the same next step every pass. This is a
requirement, not a preference: a branch that takes a different path on a later pass asks for a step
that has no recorded result at that position, and the run's recorded history no longer describes the
code that produced it.
Never branch on a value the handler reads outside a step. if (Math.random() > 0.5) and
if (Date.now() > deadline) decide differently on each pass. Record the value in a step first, then
branch on the recorded result.
See the durable execution example running end to end in Examples.