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

Reference / Workflow

Automatic durability (steps you do not have to write)

Any call that leaves the platform — an outbound client call, an external service — is made a durable step by the compiler. In a workflow, a crash-resume reuses what the call already returned instead of making it a second time. You write an ordinary call; you do not mark it, and you cannot forget to.

stable1 example compiled by CIworkflowdurabilityauthoring

Summary#

A workflow body can run more than once — that is how the platform survives a crash: work that did not commit is simply done again. Re-running ordinary computation is harmless. Re-running a payment, an email, or any call into a system that is not yours is a second charge and a second message.

Workflow.Once (run a step at most once) is the explicit way to say "not twice". You rarely need it, because the compiler already knows which calls leave the platform, and wraps each one in a durable step for you. The call you write is the call you read; the durability is not something you remember to add.

Description#

What gets a step#

Exactly one thing: a call that leaves the platform. An outbound client operation is an HTTP request to somebody else's system, so a second execution is a second request. That is the whole rule, and it is decided per call, not per function.

void Notify(Order order) {
  Mailer.Send(new SendRequest { To = order.Email });   // a durable step, automatically
}

Nothing marks it. Nothing has to.

Each call is its own step#

If a function makes two outbound calls, they are two steps, not one:

void NotifyBoth(Order order) {
  Mailer.Send(new SendRequest { To = order.Email });     // step 1
  Shipping.Book(new BookRequest { Id = order.Code });    // step 2
}

This is the part that matters. If the whole function were one step, a crash between the two calls would re-run the email on resume. Because each call is its own step, a resume finds the email already recorded, skips it, and picks up at the booking. The ordinary code between the two steps re-runs freely — it is just a computation over results that are already recorded.

What does not get a step#

Values that are merely unrepeatable — the current time, a new identifier, a random number — are not steps. Nothing about them leaves the platform, and a resumed run already sees the same value it saw the first time. They cost nothing to protect and are protected anyway.

Ordinary reads, writes and computation are not steps either. Re-running them is correct: the work that did not commit is redone, which is the point.

Outside a workflow#

The same function is often called from a workflow and from an ordinary request. There, there is no run to record against — so the call simply happens, exactly as the source reads. You do not write the function twice, and you do not choose in advance which kind of caller it is for.

When you still write it yourself#

Reach for Workflow.Once (run a step at most once) when you want something automatic durability deliberately does not do:

  • An idempotency key. A recorded result means the step is not re-run; it cannot undo a call that already reached the other system and was lost on the way back. Only that system can recognise a retry, and only from a key you pass it. Workflow.Once("step", key => …) hands you a stable one.
  • Skipping expensive but harmless work. Re-running a long pure computation is correct, just wasteful. Once is also how you say "do not redo this".
  • Your own boundary. Grouping several calls into one step, or pinning one specific result.

Writing Once around a call that would have been lowered anyway gives you one step, not two — yours, with whatever you asked for.

Examples#

enum OrderState { Placed, Fulfilled }

entity Order {
  [Required, MaxLength(60)] string Reference;
  decimal Total;
  OrderState Status = OrderState.Placed;
  security { allow read, create, update when IsAuthenticated; }
}

class ChargeResult { string? Receipt; }

// An ordinary typed client. Nothing about it says "durable".
client Payments {
  BaseUrl = "https://api.payments.example";
  [Post("/charges")]
  ChargeResult Charge([Query] string reference, [Query] decimal amount);
}

// Nothing here is marked either. The call that LEAVES the platform is what makes this a durable
// step — and `osy model --json` reports both the verdict and the route to it, so you never guess:
//   "durability": "External", "durabilityVia": "Payments.Charge"
void Fulfil(Order order) {
  Payments.Charge(order.Reference, order.Total);   // a step: never re-charged on resume
  order.Status = OrderState.Fulfilled;             // ordinary work — re-runs freely
}

An agent step and a charge, with no ceremony at all:

void Fulfil(Order order) {
  var decision = Assistant.Decide(order.Summary);   // a step: never re-charged, never re-decided on resume
  Payments.Charge(new ChargeRequest { Amount = order.Total });   // a separate step
  order.Status = Status.Fulfilled;                  // ordinary work — re-runs freely if the run resumes
}

The same call made exactly-once at the other end, by asking for a key:

void Charge(Order order) {
  Workflow.Once("charge", key => Payments.Charge(new ChargeRequest { Amount = order.Total, idempotencyKey: key }));
}

Notes#

  • The guarantee is at-least-once execution, at-most-once result. A crash in the instant after a call returns and before its result is recorded will run it again. No engine can close that window from this side — the call already happened in someone else's system. An idempotency key is the only thing that can, which is why one is offered.
  • A recorded step can outlive work that was rolled back. If the surrounding transaction rolls back after the call escaped, the record still says it ran — because it did.
  • Records belong to the run. Two runs doing the same work each do it once.
  • It composes with app versions. A recorded result is data belonging to the run, so a redeploy does not disturb it — see Deploying while workflows are running.

See also#

Related

Workflow.Once (run a step at most once)

Run something at most once per workflow run, however many times the surrounding code re-executes. The first execution…

Workflow.Run (start a workflow)

Start the workflow bound to an entity's type, on that entity. Bare — `Workflow.Run(order)` — is fire-and-forget: start…

Workflow.BeginSaga (a compensating saga scope)

Open a saga scope that couples each forward step with its compensation. `await saga.Run("step", step, () => Undo(…))`…

Deploying while workflows are running

A workflow run can outlive the deploy that started it. Deploying with --new-version freezes the code and data shape the…