# 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.

<!-- id: workflow-automatic-durability · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/automatic-durability/ -->

## Summary        {#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)](https://osysharp.com/reference/workflow/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    {#description}

### What gets a step   {#what-steps}
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.

```osy title="one outbound call becomes a step, with nothing marked" syntax
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   {#per-call}
If a function makes two outbound calls, they are **two steps**, not one:

```osy title="two outbound calls are two steps, not one" syntax
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   {#not-steps}
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   {#outside}
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   {#vs-once}
Reach for [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/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       {#examples}
```osy title="the classification is the compiler's, and it will tell you" test app=workflow-automatic-durability
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:

```osy title="a decision and a charge, with no ceremony at all" syntax
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:

```osy title="taking an idempotency key for the far side" syntax
void Charge(Order order) {
  Workflow.Once("charge", key => Payments.Charge(new ChargeRequest { Amount = order.Total, idempotencyKey: key }));
}
```

## Notes          {#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](https://osysharp.com/reference/project/app-versions/).

## See also       {#see-also}
- [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) — writing a step yourself, and the idempotency key.
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start a workflow, and optionally wait for it.
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — compensating steps, for work that must be *undone* rather than not repeated.
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — what a run in flight keeps executing across a deploy.
