Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Guides

Durable Execution — built in

Your database is transactional. The world is not — and a rollback does not undo the payment you took. So every call is classified, and the ones that leave the platform run exactly once, across a crash and across a deploy. There is no retry policy to write.

01

Why any of this matters

A rollback undoes your rows. It undoes nothing else.

Not the payment you took, not the email you sent, not the row you wrote in somebody else's system, and not the model call you were billed for. The moment your code reaches outside, a crash leaves you in a state your database cannot describe — and cannot repair.

And it will crash, because a deploy is a crash, a scale-down is a crash and an OOM is a crash — they happen on a Tuesday afternoon rather than in a disaster. The failures are not abstract either: a second charge, two identical emails to the same customer hours apart, a refund issued twice, an approval that waited three days and then executed against code that no longer exists.

02

The compiler already knows which calls leave the platform

A workflow body can run more than once — that is how a crash is survived: work that did not commit is done again. Re-running arithmetic is harmless. Re-running a payment is a second charge.

So every call that leaves the platform is wrapped in a durable step, per call, with nothing marked. A resume finds the first call already recorded, skips it, and picks up at the second — the ordinary code between them re-runs freely, because it is a computation over results that are already written down.

And the verdict is not folklore. It is in the resolved model, with the route it took:

$ osy model --json      # in demo/agent-expenses

"name": "ReadReceipt",
"effects": ["Llm.Extract", "UnitOfWork.Commit"],
"durability": "External",
"durabilityVia": "Llm.Extract"
durabilitywhat it meanson a resume
Deterministicthe same inputs give the same answerre-run, freely
Nondeterministicit reads a clock, a random number, the databasere-run, but the value it produced is recorded where it must not move
Externalit leaves the platform — a model, an HTTP call, a mailnot re-run. The recorded result is reused

03

When "do not run it twice" is not enough

Some work has to be UNDONE. Stock was reserved, a card was charged, and then the courier hand-off failed — the two committed steps have to come back. That is a saga, and it is written as a block with a lifetime rather than as a chain of callbacks.

demo/wf-order-saga/model/order.osyverbatim — this file compiles
workflow FulfillmentSaga {
  Tracks    = Fulfillment.Status;
  Autostart = true;
  Initial   = Building;

  state Building {
    enter {
1      var trace = Workflow.Once("make-trace", () => new Trace { Fulfillment = this.Item, Log = "" });
      Log.Information("▶ fulfillment {Reference}: starting saga", this.Item.Reference);
2      await using var saga = Workflow.BeginSaga();      // dispose-without-Complete unwinds the committed steps, in reverse
      try {
        Log.Information("  ✓ step 1: reserving stock");
        var reservation = Workflow.Once("make-reservation", () => new Reservation { Fulfillment = this.Item });
3        await saga.Run("reservation", reservation, () => ReleaseStock(reservation, trace));   // step 1 + its undo, coupled

        Log.Information("  ✓ step 2: charging the card");
        var charge = Workflow.Once("make-charge", () => new Charge { Fulfillment = this.Item });
        await saga.Run("charge", charge, () => RefundCharge(charge, trace));             // step 2 + its undo

4        saga.OnUnwind(() => ReleaseHold(trace));        // a compensation with NO forward step (a loyalty hold booked inline)

        Log.Information("  ✓ step 3: dispatching the courier");
        var dispatch = Workflow.Once("make-dispatch", () => new Dispatch { Fulfillment = this.Item });
        await saga.Run("dispatch", dispatch);                        // step 3 — no undo; this child FAILS

5        saga.Complete();                                 // never reached
        goto Fulfilled;
6      } catch (WorkflowError e) {
        Log.Information("✗ fulfillment {Reference}: a step failed — rolling back committed steps in reverse", this.Item.Reference);
        goto Aborted;                                    // the goto unwinds through the await-using dispose → H, C, S
      }
    }
  }

  terminal success Fulfilled { }
  terminal error   Aborted   { Message = "fulfillment aborted — reservation and charge compensated"; }
1

An explicit memo. Creating this row is not an outbound call, so nothing would have made it a step — Once is how you say "whatever else re-runs, this happened already".

2

The saga is a SCOPE. Leaving it without calling Complete() unwinds every step that committed, in reverse. await using is the ordinary C# shape, doing the ordinary C# thing.

3

The undo is coupled to the step that needs it, at the point the step is written. Not in a rollback function at the bottom of the file that has to be kept in step with the forward path.

4

A compensation with no forward step — something committed inline that still has to come back. It unwinds in the same order as the rest.

5

The only thing that keeps the work. Reaching the end of the block without it is a rollback, so the failure path is the default and the success path is the one you have to say out loud.

6

The failing child raises. The goto leaves the scope, which disposes the saga, which unwinds — refunding the charge, then releasing the stock, in reverse.

04

What happens when you change it

Durability, and the questions people actually ask about it

Deploy while a run is parked
compiles
The parked run keeps resolving against the code and data shape it started under. That is what an app version is for.
Deploy a change that would strand a parked run
refused
Refused at deploy, and a migration is generated naming what it could not decide for you.
Add a retry policy around an outbound call
refused
There is nothing to add it to. The call is already a step; a resume reuses its recorded result rather than repeating it.
Reorder two steps in a saga
compiles
The unwind order follows: it is the reverse of what committed, not a list you maintain.
Forget saga.Complete()
compiles
It rolls back. Compiles, runs, and undoes the work — which is the safe way round for the mistake to land, and why the default is that way.

Where to go next

Workflows

The state, the slots, and the run this all happens inside.

Workflow reference

`Once`, `BeginSaga`, `Run` — one page each.

wf-order-saga

The app on this page, one command away.