# complete when (a state's own completion condition)

> Declare, once on a state, the condition under which that state is finished and where the run goes next. It is re-checked after anything happens in the state, so the handlers can just record what happened instead of each one deciding whether the work is over. Reach for it when a state ends because of a condition over your own data rather than because one particular event arrived.

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

## Summary        {#summary}
Some states end because a particular thing happened. Others end because the world reached a particular shape — every
item settled, every reviewer answered, nothing left outstanding. For the second kind, the state is not waiting for an
*event*; it is waiting for a *condition*.

**`complete when (<predicate>) goto <State>;`** says exactly that, once, on the state. After anything happens in that
state the predicate is re-checked, and when it holds the run moves on.

## Signature      {#signature}
```osy syntax
state <Name> {
  complete when (<predicate over this.Item>) goto <State>;
  …
}
```
One per state. The predicate is an ordinary boolean expression with `this.Item` — the row the workflow tracks — in
scope, so it can call your own functions and read your own tables.

## Description    {#description}
Without it, the condition has to be repeated at the end of **every** handler that could be the last thing to happen:

```osy title="the same question, asked in three places" syntax app=drop-ship-order
on ShipmentDelivered(Shipment shipment) { …; when (AllItemsSettled(this.Item)) { goto Settled; } }
on ShipmentLost(Shipment shipment)      { …; when (AllItemsSettled(this.Item)) { goto Settled; } }
on CustomerWithdrawsItem(OrderItem item){ …; when (AllItemsSettled(this.Item)) { goto Settled; } }
```

That works, and it quietly gets worse as the workflow grows. The obligation lands on every handler you add, and it is
invisible at the moment it matters: **add a fourth handler, forget the line, and the run stays in that state for
ever.** Nothing reports it — there is no error, no timeout, no failed step. It simply never finishes.

Declared once, the handlers go back to doing one job each:

```osy title="declared once; the handlers just record what happened" syntax
state Fulfilling {
  // Asked ONCE, on the state — not at the end of each handler. Re-checked after anything happens here.
  complete when (AllItemsSettled(this.Item)) goto Settled;

  on ShipmentDelivered(Shipment shipment)  { shipment.Delivered = true; }
  on ShipmentLost(Shipment shipment)       { shipment.Lost = true; }
  on CustomerWithdrawsItem(OrderItem item) { item.Withdrawn = true; }
}
```

⚠ **Written out rather than pulled from an app, because no shipped sample uses `complete when` yet** — the fence is
`preview` for that reason as well as the surface's own. When a sample adopts it, this becomes a `sample=` pull like
the rest, and stops being prose that can drift.

**An explicit `goto` still wins.** A state can be left for reasons that have nothing to do with being finished — a
cancellation, a customer giving up. A handler that decides to leave goes where it says; the completion condition is
only consulted when the handler did not already transition.

**When it is checked.** After a handler for that state has run and its changes are committed — so the predicate sees
the world that handler left, including the rows it just wrote. It is not a poll: nothing re-checks it while the state
is idle, because nothing has changed.

**A condition that already holds ends the state at the first opportunity.** A job whose work is already complete is
complete; that is not a special case to guard against.

## Examples       {#examples}
```osy title="the state completes when the rule holds" test app=workflow-complete-when
enum Decision   { Approve, Reject }
enum OrderState { Fulfilling, Shipped }

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

entity Order {
  [Required, MaxLength(60)] string Reference;
  [Required] Person Requester;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

workflow Fulfilment {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Fulfilling;

  event Pack();

  state Fulfilling {
    subscribe Pack();
    on Pack { }

    Requires {
      Packed { Must    = this.Item.Reference != "";
               Message = "Every line has to be packed first."; }
    }
    on Complete { goto Shipped; }
  }
  terminal success Shipped { }
}
```

An order that is finished when every one of its items has come to rest — whichever way each one got there, and
however many shipments it took:

```osy title="waiting for a condition, not for a set of work" syntax app=drop-ship-order
state Fulfilling {
  enter { SourcePendingItems(this.Item); }

  complete when (AllItemsSettled(this.Item)) goto Settled;

  subscribe ShipmentDelivered(Shipment shipment);
  subscribe CustomerWithdrawsItem(OrderItem item);

  on ShipmentDelivered(Shipment shipment) { … }   // records deliveries
  on CustomerWithdrawsItem(OrderItem item) { … }  // records a refund
}
```

## Notes          {#notes}
**This is not a join, and the difference is worth knowing.** A join is for *"I started this specific set of work and I
am waiting for it to come back."* A completion condition is for *"I am waiting for my own data to reach a shape."* The
order above cannot use a join: the number of shipments is unknowable when it starts, shipments come and go underneath
it, and a lost one puts its items back into the pool to be sent again. What the order waits for is a fact about its
items, not a set of tasks.

**It is not valid on a `terminal`.** A terminal is where a run ends, so there is no completion left to condition;
declaring one there is a compile error rather than a line that is silently ignored.

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — declaring what a state waits for
- [fan-out (foreach subscribe)](https://osysharp.com/reference/workflow/fan-out/) — one slot per element, when you *are* waiting on a set of others
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting child work from a state
