# 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 runs the step and records its result durably; every later execution of that call site returns the recorded result without running the step again. Reach for it around anything that leaves the platform — a payment, a message, an outbound call — where running twice would be worse than running slowly.

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

## Summary        {#summary}
A workflow body can run more than once. That is not a bug — it is how the platform survives a crash: work that did not
commit is simply done again. For ordinary computation that is exactly right. For a **payment**, an **email**, or any
call into a system that is not yours, it is a second charge and a second message.

**`Workflow.Once("step", () => step)`** marks the boundary. The first time a run reaches that call site, the step runs and
its result is recorded durably. Every later time — a retry after a crash, a resume after a wait — the recorded result
comes straight back and **the step does not run**.

## Signature      {#signature}
```osy syntax
Workflow.Once("charge", () => step)              // this step's LABEL, then the step: run at most once
Workflow.Once("charge", key => step)            // …and receive an idempotency key to pass on
Workflow.Once("charge", order.Id, key => step)  // …with that key scoped to the order, not to this run

Workflow.Once("charge", () => step,             // …and try again if it FAILS, on a growing wait
              retry: Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4))
```

The argument is a **lambda**, and that is load-bearing: the step must not run until `Once` has checked whether it
already did. Writing `Workflow.Once("charge", Charge(total))` would evaluate `Charge` first — the very thing being prevented — so
it is a compile error.

The result type is the lambda's own, so `Once` can be wrapped around an existing expression without changing anything
around it.

## Description    {#description}

### What it guarantees, precisely   {#guarantees}
- **The result is recorded once and reused.** A re-execution returns the first result, so everything downstream sees
  the same value it saw the first time and cannot diverge.
- **A step that returned nothing still counts as run.** A recorded empty result is a hit, not a miss — a notification
  that returns nothing does not send twice.
- **The record survives a rollback.** It is committed separately from the surrounding work, on purpose: if it were
  written with the rest of the step's transaction, a crash before that transaction committed would take the record down
  with it and the retry would run the step again.

### What it does not guarantee   {#limits}
**A crash in the instant between the step returning and its result being recorded will run the step again.** That
window is small, but it is real, and no workflow engine can close it — the effect happened in someone else's system,
and there is no way to know it happened without a record of it. Every durable engine draws the line in the same place.

Closing it takes the other side's help, in the form of an **idempotency key** it can use to recognise a retry. Take one
by giving the lambda a parameter:

```osy syntax
var receipt = Workflow.Once("charge", key => Payment.Charge(total, idempotencyKey: key));
```

The key is derived for you, and it is the same key every time this step of this run is attempted — which is exactly
what makes the receiving system able to spot the second attempt. It is unique per step and per run, so a loop over
three lines presents three keys and a second order presents different ones again. It is opaque: it carries nothing
about your code.

`Once` gives you a stable result; the key gives you a single effect.

### `retry:` — trying again when the step FAILS {#retry}
`Once` is about a step not repeating. **`retry:` is about a step that did not succeed at all.** They are opposite
halves of the same question and they compose: a retried step still records its result the first time it works, and
still returns that record for ever after.

```osy title="retrying a step that FAILED, not one that repeated" syntax
Workflow.Once("charge", () => Payment.Charge(total),
              retry: Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4))
```

Without `retry:`, a step that throws ends the body — the fault travels out to whoever drove the run (the person who
clicked the button, the timer that fired) and the run stays where it is. With it, the run **parks on a durable clock**
and runs the body again when the wait elapses, up to the policy's `.MaxAttempts`.

The wait is durable rather than a pause, and that is not an implementation detail you can ignore: the body runs
inside whatever request drove it, so a five-minute backoff taken in-process would hold a person's browser open for
five minutes. Parking means the caller returns immediately and the run resumes on its own.

**`.MaxAttempts(n)` is required here**, unlike on a milestone — a milestone has `Retries = N` counting for it, and a
step has nothing else in scope, so an uncapped policy would retry for ever:

```console
a step's `retry:` policy must cap its attempts with `.MaxAttempts(n)` — without one the step would be retried
for ever. A milestone can leave it out because its `Retries = N` supplies the budget; a step has nothing else in
scope. Write `Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4)` — that is 4 attempts in TOTAL, so 3
retries after the first try.
```

#### What a retry re-runs, and what it does not   {#retry-scope}
The retry **re-enters the body from the top**, rather than resuming at the step. That sounds like more work than
necessary and is the only shape that is correct: a step that failed halfway may have written rows, and resuming at
the step would have to commit them. So the failed attempt's writes are discarded entirely, the body runs again, and
**every step that already completed returns its record instead of running** — which is exactly what `Once` is for.
Work between the steps is re-executed by the same code that did it the first time.

⚠ So `retry:` and `Once` are not independent decorations. A retried body that does effectful work *outside* a step
will do that work again on every attempt. Put anything that must not repeat inside its own `Once`.

#### Which failures are retried   {#retry-which}
Everything except the ones that cannot succeed on a second attempt: a row that was **not found**, a value that
**failed validation**, a **conflict**, an **authorization refusal**, an **unmet requirement**, and a **cancelled**
child workflow. Those are the platform having decided something, and waiting changes none of them.

There is no predicate to write. Classifying exceptions at the call site would be a second way to spell `catch`, in a
position where you cannot see the body it guards — so `retry:` stays a single value, and anything more specific is
written with the `try`/`catch` you would already reach for.

#### A retried step may not `await`   {#retry-no-await}
A retry parks on a **clock** and re-runs the body from the top; an `await` parks on a **cursor** and resumes where it
left off. One run cannot hold both waits, so a step carrying `retry:` whose body awaits is a compile error rather
than a policy that quietly stops applying the moment the body suspends. Let the child workflow carry its own retry,
or move the awaited call out of the step.

#### `try`/`catch` sees the LAST failure, not each one   {#retry-catch}
A `catch` around a retried step is asking *what to do when this has failed for good*. It does not run per attempt: the
retry happens inside the step, and only when the attempts are spent does the original exception come out — at which
point your handler sees exactly what it would have seen had no policy been written.

### When the retry is a whole new run   {#new-run}
The key above is scoped to **this run**, which is right for "this run's step is being attempted again". It is not what
you want when the second attempt is a *different* run against the same thing — a failed order retried tomorrow, a
re-submitted invoice. Those are two runs, so they derive two keys, and the charge happens twice.

Say what the work is really identified by, as an ordinary first argument, and **name the step**:

```osy title="scoping the key to the order, so a second run matches" syntax
Workflow.Once("charge", order.Id, key => Payment.Charge(order.Total, idempotencyKey: key));
```

Now any run reaching that step for **that order** presents the same key, and the receiving system recognises the
second one. Two things stay true and are worth being precise about:

- **The step still runs.** Scoping changes the key, not the record — a record belongs to a run and is cleaned up with
  it. What you gain is that the far side refuses to act twice, which is the only place a single effect can be decided.
- **Different steps under one scope still get different keys.** Otherwise a refund under the same order would present
  the charge's key and be swallowed as a duplicate — a payment that never happens, which is worse than one that
  happens twice, because nothing reports it.

### Why the scoped form makes you name the step   {#scoped-name}
The name is not decoration, and it is the reason the two bullets above can both be true. Something has to tell the
`"charge"` step apart from the `"refund"` step under the same order, or they derive one key and the refund is
swallowed.

That job used to be done by **where the step sat in your code** — which worked until you edited the function around
it. Then the key changed, and a run started after the edit was no longer recognised as a retry of one started before:
exactly the case a scope exists for, since two runs far enough apart to matter will usually straddle a deploy. A name
you chose does the same job and survives the edit, so the promise holds across versions. That is why the scoped form
requires one and the plain form has no use for it.

Pick names that describe the work — `"charge"`, `"send-invoice"` — and treat them as durable: **renaming one is a new
key**, and a run mid-flight against the old name will not recognise the new one.

### Where it can go   {#where}
Anywhere in a workflow body: a state's `enter`, an event arm, a timer body, a saga step, or a function the body calls.
Inside any loop — `foreach`, `while`, `for` — each iteration is its own step, so a loop that charges once per line
charges once per line, and a retry re-charges none of the lines it already did.

A step is identified by the **path taken to reach it**, not only by the line it sits on — the body the run entered,
then the name of each function called along the way, then the step's own label. An operator reading a run's records
sees exactly that: `Approved.enter/Charge#charge-card`.

Every part of it is a name you chose, so editing the code around a call does not move it. That is what lets a run
that started before a deploy recognise the work it already did. The one exception is a loop, which contributes the
iteration number — there is nothing in your source to name there, and "which pass over the data" is the honest
identity.

Because a path segment is a *name*, calling one helper **twice from the same body** would give the steps inside it
one identity, so that is a compile error. Loop over the things you are acting on and each pass is its own step; or,
if the two calls are really different work, give them their own functions.

(That path identifies a step's *record*. A scoped step's outward idempotency key is identified by its label
instead — see above.)

### Every step carries a label {#label}
The first argument is the step's **label** — a literal string you choose, naming what this step does. It is required.

The label is the step's **durable identity**. The platform records "this step already ran" against it, so the label is
how a later execution finds that record. It used to be derived from where the step sat in the code, which worked
within one deployed version and broke across a new one: edit anything above the step and its identity moved, the
record was no longer found, and the step ran a second time. For a payment or a message, a second run is the whole
problem this construct exists to prevent.

A name you chose does not move when you edit the code around it. That is the entire reason it is not optional.

```osy syntax
Workflow.Once("charge-card", key => Payments.Charge(total, idempotencyKey: key));
Workflow.Once("send-receipt", () => Email.Receipt(order));
```

Two steps in the **same body** may not share a label — they would share one identity, and the second would read
back the first's recorded result and never run. That is a compile error.

A **body** here means one block the platform runs as a unit: a function, or one of a workflow's own — its `Start`, a
state's `enter`, a single event arm, a milestone hook, a reminder. Each is a separate identity, so the same label in
a state's `enter` and in an event arm is two different steps, and is fine. So is the same label in two different
functions. Labels are compared exactly.

### You do not have to write one — until two calls collide {#unlabelled}
Most external calls need no `Once` at all. Write the call plainly and the platform wraps it in a step for you, naming
that step after the call:

```osy title="one plain call needs no label at all" syntax
Mailer.Send(receipt);          // one step, named for `Mailer.Send`. Nothing to write, nothing to maintain.
```

That name is a complete identity while there is **one** such call in the body — nothing about `Mailer.Send` moves,
however much you edit around it. Two calls to the *same* operation in one body have no way to be told apart, so the
compiler asks you to name them:

```osy title="✗ two calls to the same operation must be named" syntax
Mailer.Send(primary);          // ✗ two unlabelled steps, both named for `Mailer.Send`
Mailer.Send(backup);
```
> two external steps here both go through `Mailer.Send`, so neither carries an identity that survives an edit …
> Name them: `Workflow.Once("a-name-for-this-one", () => Mailer.Send(…))`

Name either one and the ambiguity is gone — the labelled step takes your name and the remaining plain call keeps the
derived one. Calls to *different* operations never collide, so they need nothing.

**Why it is refused rather than guessed at.** Numbering them by position works right up to the edit that matters:
insert an earlier call to the same operation and the later one renumbers, so a run resuming after that deploy no
longer recognises the step it already did — and sends twice. That is the failure this whole construct exists to
prevent, so the platform declines to guess.

## Examples       {#examples}
Charging a card — the case the construct exists for:
```osy title="charging a card — the case the construct exists for" syntax
state Approved {
  enter {
    var receipt = Workflow.Once("charge", () => Payment.Charge(this.Item.Total));
    this.Item.ReceiptId = receipt.Id;
    goto Shipped;
  }
}
```
If the process dies after the charge but before `goto Shipped` commits, the run retries `enter` from the top — and the
charge does not happen again.

One step per item — and the same in any loop:
```osy title="a loop gives every line its own step" syntax
foreach (var line in this.Item.Lines) {
  Workflow.Once("reserve", () => Fulfilment.Reserve(line.Sku, line.Quantity));
}
```
Each line is its own step: a retry re-reserves none of the lines already reserved, and still reserves the rest.

A step that has to survive a flaky dependency — the whole workflow, compiled:

```osy title="a durable step with a retry policy" test app=workflow-once-retry
enum OrderStage { Placed, Charged, Failed }

[Principal] entity Person {
  [Required, MaxLength(80)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Order {
  [Required, MaxLength(120)] string Reference;
  OrderStage Stage;
  decimal Total;
  [MaxLength(200)] string ReceiptId;
  security { allow read, create, update when IsAuthenticated; }
}

// Stands in for the payment gateway. Anything that can be briefly unavailable belongs behind a step.
string Charge(Order order) {
  return "receipt-" + order.Reference;
}

workflow OrderFlow {
  Tracks    = Order.Stage;
  Autostart = true;
  Initial   = Placed;

  event Pay();

  state Placed {
    subscribe Pay();
    on Pay {
      // Four attempts in total — 2s, then 4s, then 8s — and never twice on success, because it is still a step.
      this.Item.ReceiptId = Workflow.Once("charge", () => Charge(this.Item),
                                          retry: Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4));
      goto Charged;
    }
  }

  terminal success Charged { Message = "paid"; }
  terminal error   Failed  { Message = "could not charge"; }
}
```

Skipping expensive work that is merely slow:
```osy title="skipping work that is slow rather than external" syntax
var report = Workflow.Once("build-quarterly-report", () => BuildQuarterlyReport(this.Item));
```
Nothing here leaves the platform, so re-running would be *correct* — just wasteful. `Once` is also how you say "do not
redo this."

## Notes          {#notes}
- **A recorded step can outlive work that was rolled back.** If the surrounding transaction rolls back after the step
  escaped, the record still says it ran — because it did. Surprising once; correct every time.
- **`Once` is about repetition, not waiting.** It does not pause the run and is not a substitute for
  [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/)'s durable wait. A step that itself waits can be wrapped in one.
- **Records belong to the run** and are cleaned up with it. That stays true even with a scope: what a scope shares
  across runs is the KEY, never the record — so two runs doing the same work each still do it once, and it is the
  receiving system that declines the second.
- **It composes with app versions.** A recorded result is data belonging to the run, so it is unaffected by a
  redeploy — see [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/).

- **You often do not need to write it.** A call that leaves the platform is made a step by the compiler — see
  [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/). Writing `Once` around one of those gives you one step, not two. Reach for the
  explicit form when you want an idempotency key, your own boundary, or to skip expensive-but-harmless work.

## See also       {#see-also}
- [Backoff (retry policy)](https://osysharp.com/reference/workflow/backoff/) — the retry policy `retry:` takes, and what `.MaxAttempts` / `.Cap` / `.Jitter` mean
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — how a step's label, its memo and its call path survive a deploy
- [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/) — the steps the compiler writes for you, and when to write your own.
- [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.
