# Workflow.Once (run a step at most once)

> Run something at most once per execution of its body, however many times a crash, a retry or a resume makes that execution run again. The first attempt runs the step and records its result durably; every later attempt of the same execution returns the recorded result without running the step again, while the body's NEXT execution (a state entered again, an event arm fired again) is new work and runs it anew. 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 an execution of the body reaches that call
site, the step runs and its result is recorded durably. Every later attempt of that execution — a retry after a crash,
a resume after a wait — gets the recorded result straight back and **the step does not run**. A later execution of the
body (the state entered again, the arm fired again) is new work, and runs it again — see [executions](#executions).

## 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`.
That includes a value that is different each time it is asked: a `DateTime.UtcNow` or `Guid.NewGuid()` read above the
retried step is read again on every attempt, and `osy lint` says so (`workflow-value-read-again-on-retry`) when that
value is then stored — see [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/).

#### A retry asks everything inside the step again   {#retry-attempts}
"Retry this step" means **the whole step**. Each attempt is a new execution for everything *inside* it — including a
step nested in it, and the outbound call a helper makes, which the platform turns into a step of its own
([Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/)). So a step whose helper posts to a payment processor and throws on a `503` posts
again on the retry, and succeeds on the answer it gets then:

```osy title="a retried step whose helper makes the call" syntax
string AskProcessor(Order order) {
  var answer = Http.Post("https://pay.example/charge", order.Reference, "text/plain");
  if (answer.StatusCode != 200) { throw new Exception("the processor answered " + answer.StatusCode); }
  return answer.Body;
}

// …in a workflow body:
this.Item.ReceiptId = Workflow.Once("charge", () => AskProcessor(this.Item),
                                    retry: Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4));
```

Two things stay exactly as the rest of this page promises:

- **The same attempt delivered again is the same attempt.** A crash after the inner call was recorded but before the
  attempt committed runs that attempt again, and the call it already made is read back, not made twice.
- **A step outside the retried one is untouched.** A step that completed *before* it in the body still replays its
  record on every attempt.

The operator-visible ids say which attempt a nested step belongs to:
`OrderFlow/Placed.enter@1/charge~1/AskProcessor#auto:Http.Post` is the call `AskProcessor` made during the **first
retry** of `charge`, on the first entry to `Placed`. The first try carries no `~` segment.

⚠ **A step whose body makes several outbound calls repeats all of them.** If one of them must not be made twice — a
charge before a confirmation email — give it its own step outside the retried one, so it is recorded once and replays.
`osy lint` points at a retried step that reaches more than one outbound call (`workflow-retried-step-repeats-several-calls`).

#### 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
and which execution of it this is, 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@1/Charge#charge-card` — the first entry to
`Approved`, through `Charge`, at the step labelled `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.

### Each execution of a body is its own set of steps {#executions}
"At most once" means once per **execution** of the body, not once per run. A run executes one body many times
whenever it comes back to it, and every one of those is new work that must reach the outside world again:

- an **event arm** that fires twice while the run stays in its state;
- a state's **`enter`** on a second visit — a run that goes `Asking → Resting → Asking`;
- a state's **clock route** (`on Expire`) when the state is entered again and its clock re-armed;
- each firing of a **repeating reminder** (`Remind Poll(After = …, ThenEvery = …)`).

So each execution is numbered — its **visit** of that body, counted per body on the run — and the number is part of
every step's identity: `Asking.enter@1#…` on the first entry, `Asking.enter@2#…` on the second. The same number
qualifies the label of a child run the body starts (`await Workflow.Run("check", c)`), so a second visit starts a
second child rather than being handed the first visit's finished one.

What is **not** a new execution is the one case `Once` exists for — the same execution delivered again:

- a body that **faulted** and is delivered again (an event re-raised after its arm threw, a clock route fired again
  after it threw): the fault rolled the run back, the visit was never counted, and the redelivery replays the calls
  the failed attempt already made;
- a **`retry:`** re-entry, and the **resume** after an `await`: the park carries its visit, so the steps before the
  fault or the wait are found, not made again. (The steps *inside* the retried step are the exception, and on
  purpose: each attempt of it is new work for them — see [what a retry asks again](#retry-attempts).)

A visit is counted only once its execution is finished with — completed, parked, or (for a reminder, whose clock
re-arms whatever happened) faulted — and it is committed with that execution's own work, so a crash anywhere before
the commit leaves the count where it was.

A run that was **already parked when this numbering arrived** keeps the identities it recorded under: its park carries
no visit, so it resumes under ids with none, finds the steps it completed, and every execution after it is numbered.

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

### Two calls in EXCLUSIVE branches are one call {#exclusive-branches}
The rule above counts calls that can **both run**. Two arms of one `if` / `else if` / `else`, or two sections of one
`switch`, never both run — so a helper called from each arm is not a collision and is not refused:

```osy title="notify whichever of these happened — one helper, three arms" syntax
if (this.Item.Severity >= 3)      { NotifyOne(this.Item.Reporter, "critical"); }
else if (this.Item.Severity == 2) { NotifyOne(this.Item.Reporter, "major"); }
else                              { NotifyOne(this.Item.Reporter, "minor"); }
```

They share one step identity, and that is safe rather than tolerated: a memo belongs to **one run**, and in any one
run exactly one arm executes, records under that identity, and is the arm that reads it back when the run resumes.

⚠ **What is still refused is a call that can run ALONGSIDE another** — one inside an arm and one after the whole
`if`, or two in the same arm. Both really do execute on the same path, so the second would read back the first's
record:

```osy title="✗ both of these run whenever the branch is taken" syntax
if (this.Item.Severity >= 3) { NotifyOne(this.Item.Reporter, "critical"); }
NotifyOne(this.Item.Reporter, "always");     // ✗ called twice — the refusal names the other line
```

`try` / `catch` is deliberately not treated as a choice: a body can run partway and then its `catch` runs too, in the
same pass.

## 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
using Osysharp.Workflow;

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.
