# Workflow.BeginSaga (a compensating saga scope)

> Open a saga scope that couples each forward step with its compensation. `await saga.Run("step", step, () => Undo(…))` runs a child-workflow step and registers its undo in one call; if a later step fails, disposing the scope without `saga.Complete()` runs the registered undos in reverse. It is the C# `TransactionScope` idiom — `await using` + a durable undo stack — for a multi-step saga whose plain `try`/`catch` would otherwise force a compensation staircase.

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

## Summary        {#summary}
**`Workflow.BeginSaga()`** opens a **saga scope** — a durable transactional scope for a **multi-step** saga. Its one
load-bearing idea is **coupling**: `await saga.Run("step", step, () => Undo(step))` runs a step **and** registers its
compensation in a single call, so a step can never run without its undo. If any later step fails, disposing the scope
**without** calling `saga.Complete()` runs every registered undo **in reverse order**.

Reach for it only for the **complex** case. A **simple** saga — one step, one compensation — is cleaner as a plain
`try`/`catch` around [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) (nothing to couple). A saga with N steps that must unwind in reverse is where
pure `try`/`catch` forces a **compensation staircase** — each step's `catch` re-listing every earlier undo — and that
duplicated, hand-ordered list is exactly the bug-farm the scope removes.

## Signature      {#signature}
```osy syntax
await using var saga = Workflow.BeginSaga();   // open the scope; dispose-without-Complete unwinds, in reverse
await saga.Run("step", step, () => Undo(args));        // run a child-workflow step AND register its compensation
await saga.Run("step", step);                          // a step with no compensation (e.g. the last one)
saga.OnUnwind(() => Undo(args));               // register a compensation with NO forward step
saga.Complete();                               // every step stood → drop the undo stack (dispose becomes a no-op)
```

`step` is an entity-typed value whose type has exactly one workflow bound to it — the same inference [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/)
uses. A compensation lambda's body is a single call: an app **function** (`() => VoidAuth(auth)`) or a compensating
**child workflow** (`() => Workflow.Run(new ReturnShipment { Shipment = s })`).

## Description    {#description}
`await using var saga = Workflow.BeginSaga();` is a C# **using declaration**: the scope is disposed at the end of the
enclosing block, on **every** exit path — normal fall-through, a `goto` to a terminal, or a thrown fault. Disposal runs
the saga's registered compensations **unless** `saga.Complete()` was reached first.

**Each step registers its undo, and only committed steps compensate.** `await saga.Run("step", step, () => Undo(step))` starts
the child workflow bound to `step` and **waits** for it (a durable wait — the flow parks and resumes exactly like an
awaited [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/)). On the step's **success**, its undo is pushed onto the scope's stack; on a **failure**, the
call throws **`WorkflowError`** (or **`WorkflowCancelled`**) and registers **nothing** — a step that never committed is
never compensated. So a failing step propagates its fault to your `catch`, and the scope unwinds the steps that *did*
stand.

**Reverse order is automatic, and disposal runs before the terminal transition.** The undo stack is LIFO, so
compensations run newest-first. Because a workflow `goto` is deferred to the end of the body, the `await using`
disposal runs its compensations **before** the routing `goto` takes effect — you compensate, then route, with no
explicit unwind call.

**A compensation is a function or a child workflow.** Same step-granularity rule as a forward step: a DB-only or
single-call undo is an app **function** the saga names; a multi-step, waiting undo (a real return: pickup → inspect →
refund) is a **child workflow** (`() => Workflow.Run(new Return { … })`). A child-workflow undo **waits** too — disposal
parks on it and resumes when it terminals, then continues unwinding the rest of the stack.

**`saga.OnUnwind(() => Undo(…))`** registers a compensation that has **no** forward step — useful when something you did
outside a `saga.Run` (a side effect earlier in the body) still needs undoing if the saga rolls back. It pushes onto the
same stack, in call order.

**`saga.Complete()`** marks the saga successful: it drops the undo stack, so the `await using` disposal becomes a no-op.
Call it once every step has stood, just before you route to the success terminal.

**The scope is durable.** The undo stack rides the parked continuation, so a saga that waits across steps (or across a
restart) resumes with its registered compensations intact.

## Examples       {#examples}
A four-step fulfillment saga, shown as the complete app the docs gate compiles. Each step is a child workflow that waits
on the outside world; the first three couple a plain-function compensation, the shipment step's compensation is itself a
child workflow (a real multi-step return), and `saga.OnUnwind` registers one more undo that has no forward step. If any
step fails, the committed steps unwind in reverse and the order routes to the error terminal.

First the world the saga runs in — the order and its customer, the four workflow-bound step entities, the compensating
`ReturnShipment` child, and the compensation functions the saga names:

```osy title="the app the saga lives in" test app=wf-beginsaga-example
enum OStatus     { Received, Shipped, Rejected, Cancelled }
enum StepStatus  { Waiting, Done }

entity Customer { [Required, MaxLength(60)] string Name; decimal LastRefund; }
entity Order    { [Required] Customer Customer; decimal Total; OStatus Status = OStatus.Received; }

entity PaymentAuth          { [Required] Order Order; decimal Amount; StepStatus Status = StepStatus.Waiting; bool Undone; }
entity InventoryReservation { [Required] Order Order; StepStatus Status = StepStatus.Waiting; bool Undone; }
entity WarehousePick        { [Required] Order Order; StepStatus Status = StepStatus.Waiting; bool Undone; }
entity Shipment             { [Required] Order Order; StepStatus Status = StepStatus.Waiting; }
entity ReturnShipment       { [Required] Shipment Shipment; StepStatus Status = StepStatus.Waiting; }

void VoidAuth(PaymentAuth auth)                        { auth.Undone = true; }
void ReleaseReservation(InventoryReservation reserve)  { reserve.Undone = true; }
void CancelPick(WarehousePick pick)                    { pick.Undone = true; }
void RevokeLoyalty(Customer customer, decimal amount)  { customer.LastRefund = amount; }

workflow PaymentAuthFlow { Tracks = PaymentAuth.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow InventoryReservationFlow { Tracks = InventoryReservation.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow WarehousePickFlow { Tracks = WarehousePick.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow ShipmentFlow { Tracks = Shipment.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow ReturnShipmentFlow { Tracks = ReturnShipment.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
```

Then the saga itself — one `await using` scope in the order workflow's `enter` body, coupling each step with its undo:

```osy title="a four-step fulfillment saga" test app=wf-beginsaga-example
workflow OrderFlow {
  Tracks = Order.Status; Autostart = false; Initial = Received;
  state Received {
    enter {
      await using var saga = Workflow.BeginSaga();          // dispose-without-Complete unwinds, in reverse
      try {
        var auth = Workflow.Once("make-auth", () => new PaymentAuth { Order = this.Item, Amount = this.Item.Total });
        await saga.Run("auth", auth, () => VoidAuth(auth));          // run step 1 + register its undo, atomically

        var reservation = Workflow.Once("make-reservation", () => new InventoryReservation { Order = this.Item });
        await saga.Run("reservation", reservation, () => ReleaseReservation(reservation));

        var pick = Workflow.Once("make-pick", () => new WarehousePick { Order = this.Item });
        await saga.Run("pick", pick, () => CancelPick(pick));

        var shipment = Workflow.Once("make-shipment", () => new Shipment { Order = this.Item });
        await saga.Run("shipment", shipment, () => Workflow.Run(new ReturnShipment { Shipment = shipment }));  // undo = a child workflow

        saga.OnUnwind(() => RevokeLoyalty(this.Item.Customer, this.Item.Total));   // an undo with no forward step

        saga.Complete();                                     // every step stood → drop the stack
        goto Shipped;
      } catch (WorkflowError e)     { goto Rejected; }        // dispose unwinds the committed steps (reverse), then routes
        catch (WorkflowCancelled e) { goto Cancelled; }
    }
  }
  terminal success Shipped   { }
  terminal error   Rejected  { Message = "order rejected"; }
  terminal error   Cancelled { Message = "order cancelled"; }
}
```

## Notes          {#notes}
- **Simple vs complex.** One step, one compensation → a plain `try`/`catch` around [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) is cleaner; the
  scope earns its place only when N steps must unwind in reverse (it removes the compensation staircase).
- **Couple the undo with the step.** `await saga.Run("step", step, undo)` is the point — you cannot run a step and forget its
  compensation, and you never hand-maintain a reverse-ordered undo list.
- **Only committed steps compensate.** A step that throws registered no undo, so it is not compensated; the steps
  before it are.
- **`await` is the wait.** `await saga.Run("step", ...)` and `await saga.DisposeUnwind()` (the disposal the `await using`
  generates) are durable waits — the flow parks and resumes, even across a restart. `saga.OnUnwind` and `saga.Complete`
  are not waits.
- **`Complete()` or it rolls back.** Reaching `saga.Complete()` is what commits the saga; any exit before it (a fault,
  or a `goto` out of the block) runs the compensations.

## See also       {#see-also}
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — what a deploy does to a saga parked halfway through its steps
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start a workflow on an entity; the awaited form is a single durable step (the simple-saga case).
- <span class="planned" title="this page is planned and not written yet">workflow-goto</span> — transition within a workflow body (deferred to body-end, which is why disposal compensates first).
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — read a workflow's timeline of events.
- [Step labels (naming a child run so it survives a new version)](https://osysharp.com/reference/workflow/park-label/) — naming a step so a run parked on it survives a new version of this saga.
