# An entity's lifecycle — a workflow that tracks its status, or one function per move

> A row that moves through states, like an order going from placed to paid to shipped, has two good shapes. A workflow with `Tracks = Order.Status` owns the field: only its transitions move it, a state can carry a deadline, and `OrderLife.For(order).Transitions` lists the moves a screen may offer. When no step waits on a clock or a person, write one function per move, each assigning the field itself, and name them all in `deny update Status except via Ship, Deliver;`. Don't write one `Advance(string to, …)` that takes the target state as text.

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

## Summary        {#summary}
Many entities have a status that moves through states in a fixed order. An order goes from placed to paid to shipped to
delivered, and may be cancelled or lapse on the way. Two shapes handle this well, and both let you say which moves are
legal in one place:

- **A workflow that tracks the field.** `Tracks = Order.Status;` hands the field to the workflow. Each state lists the
  events it accepts and where each one leads. Nothing else may write the field, a state can carry a deadline, and
  `OrderLife.For(order).Transitions` gives you the legal next moves for buttons. Use this when a step waits on a person
  or a clock.
- **One function per move.** `Ship(parcel, tracking)` and `Deliver(parcel)` each check the current state, then assign
  the field themselves. `deny update Status except via Ship, Deliver;` names every function that may change it. Use
  this for a short, immediate lifecycle with nothing to wait for.

Avoid the third shape: one `Advance(string orderId, string to, …)` that takes the target state as text, looks it up by
name, and takes the arguments of every move. Its callers fill the arguments they don't need with `""`, and a misspelt
state name either fails or, if the lookup falls back to a member, moves the row somewhere real.

## Signature      {#signature}
```osy syntax
workflow <Entity>Life {
  Tracks    = <Entity>.<StatusField>;
  Autostart = true;
  Initial   = <FirstState>;
  event <Move>(<what the move needs>);
  state <State> { subscribe <Move>(…); on <Move> { goto <NextState>; } }
  terminal success <End> { }
}

// …or, with nothing to wait for:
deny update <StatusField> except via <Move1>, <Move2>;
[Server, Authorize(<policy>)] void <Move1>(<Entity> row, <what it needs>) { …check…; row.<StatusField> = <Enum>.<Next>; }
```

## Description    {#description}

### Which shape do I want?   {#which}
| the lifecycle | write |
|---|---|
| a step waits on a person, a payment, a reply, or a time limit ("lapses after 48 hours") | a **workflow** tracking the field |
| a screen should show only the moves this row can make now | a **workflow**: `<Wf>.For(row).Transitions` lists them |
| every move happens at once, in one request, with no waiting | **one function per move**, named in `except via` |
| the moves share a rule ("only from Packed") | a helper that **checks** and returns a reason, called by each move. A helper may not write the field: `except via` excepts only the named function's own assignments |

Both shapes are enforced on the server. A workflow's field is written only by its transitions. With `except via`, the
field is written only by the functions the rule names, so a browser, an import, or any other function trying to set
`Status` is refused.

### Why not one function that takes the state as a string?   {#not-one-advance}
It looks economical, but it moves three jobs into the caller:

- **The move is chosen by text.** `Advance(id, "Shiped", …)` compiles. A lookup that falls back to a member, such as
  `return OrderState.Cancelled;` after a loop over the names, turns the typo into a real state. Use
  `Enum.Parse<OrderState>(text)`, which refuses an unknown name (see [Enum.Parse<T> and Enum.TryParse<T> — text into a member](https://osysharp.com/reference/enum/parse/)). Better, take an `OrderState`
  parameter, or write one function per move, so a wrong name cannot be written.
- **The signature can't say what each move needs.** Shipping needs a carrier and a tracking number, a refund needs an
  amount, and cancelling needs neither. One signature for all of them means every call passes `""` and `0` for the rest,
  and nothing tells the caller which blanks matter.
- **The legal-move table is written by hand.** `(from == Paid && to == Accepted) || …` is the graph a workflow already
  has, without its deadlines or `Transitions`.

The linter reports each of these: `correctness-enum-name-with-a-silent-default`,
`correctness-call-padded-for-another-move`, and, as a suggestion, `workflow-lifecycle-written-by-hand`.

### "Only one function may change the status"   {#one-writer}
If you want the database to enforce the status, you don't need a single function to do it. `except via` accepts
several names, and the rule is just as tight with many: `deny update Status except via Pay, Ship, Deliver, Cancel;`
lists every way the field changes, where a reader of the entity will see it. Each named function must make the
assignment itself, so the compiler refuses a name whose helper does the writing. The refusal names the helper and
shows this shape.

## Examples       {#examples}

### A workflow that owns the order's status   {#example-workflow}

```osy title="an order moves only along its workflow, and lapses unpaid" run app=workflow-entity-lifecycle
using Osysharp.Workflow;

enum OrderStatus { Placed, Paid, Shipped, Delivered, Lapsed, Cancelled }

entity Order {
  [Required, MaxLength(40)] string Reference;
  OrderStatus Status;                       // no default: the workflow supplies it
  [MaxLength(80)] string? Tracking;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow OrderLife {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Placed;

  event Pay();
  event Ship(string tracking);
  event Deliver();
  event Cancel();

  state Placed {
    Expire = TimeSpan.FromDays(2);          // unpaid for two days: the clock moves it
    subscribe Pay();
    subscribe Cancel();
    on Pay    { goto Paid; }
    on Cancel { goto Cancelled; }
    on Expire { goto Lapsed; }
  }
  state Paid {
    subscribe Ship(string tracking);
    on Ship(string tracking) {
      this.Item.Tracking = tracking;
      goto Shipped;
    }
  }
  state Shipped {
    subscribe Deliver();
    on Deliver { goto Delivered; }
  }
  terminal success Delivered { }
  terminal cancel  Lapsed    { Message = "not paid in time"; }
  terminal cancel  Cancelled { Message = "cancelled before payment"; }
}

/// The buttons a screen shows: only the moves this order can make now.
List<string> NextMoves(Order order) =>
  OrderLife.For(order).Transitions.Where(t => t.Allowed).Select(t => t.Event).ToList();

[Test]
void An_order_moves_only_along_its_workflow() {
  var order = new Order { Reference = "A-1" };
  UnitOfWork.Commit();
  Assert.Equal(OrderStatus.Placed, order.Status);
  Assert.False(NextMoves(order).Contains("Ship"));     // not offered before it is paid

  OrderLife.RaisePay(order);
  Workflow.Settle(order);
  Assert.True(NextMoves(order).Contains("Ship"));

  OrderLife.RaiseShip(order, "TRK-9");
  Workflow.Settle(order);
  Assert.Equal(OrderStatus.Shipped, order.Status);
  Assert.Equal("TRK-9", order.Tracking);
}

[Test]
void An_unpaid_order_lapses_on_its_own() {
  var order = new Order { Reference = "A-2" };
  UnitOfWork.Commit();
  TestClock.Advance(TimeSpan.FromDays(3));
  Workflow.Settle(order);
  Assert.Equal(OrderStatus.Lapsed, order.Status);
}
```

### One function per move, when nothing waits   {#example-verbs}

```osy title="a parcel changes status only through its moves" run app=workflow-entity-lifecycle-verbs
enum ParcelStatus { Packed, Shipped, Delivered, SentBack }

[Principal] entity Clerk {
  [Unique, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; }
}

entity Parcel {
  [Required, MaxLength(40)] string Reference;
  ParcelStatus Status = ParcelStatus.Packed;
  [MaxLength(80)] string Tracking = "";
  security {
    allow read, create, update when IsAuthenticated;
    deny update Status except via Ship, Deliver, SendBack;   // every way the status changes, in one place
  }
}

/// The rule the moves share. It only checks and never writes, so it needs no exception of its own.
string NotIn(Parcel parcel, ParcelStatus expected) =>
  parcel.Status == expected ? "" : $"{parcel.Reference} is {parcel.Status.Label}, not {expected.Label}.";

/// Each move takes only what it needs and assigns the status itself.
[Server, Authorize(IsAuthenticated)]
void Ship(Parcel parcel, string tracking) {
  var why = NotIn(parcel, ParcelStatus.Packed);
  if (why != "") { throw new Exception(why); }
  parcel.Tracking = tracking;
  parcel.Status = ParcelStatus.Shipped;
}

[Server, Authorize(IsAuthenticated)]
void Deliver(Parcel parcel) {
  var why = NotIn(parcel, ParcelStatus.Shipped);
  if (why != "") { throw new Exception(why); }
  parcel.Status = ParcelStatus.Delivered;
}

[Server, Authorize(IsAuthenticated)]
void SendBack(Parcel parcel) {
  var why = NotIn(parcel, ParcelStatus.Shipped);
  if (why != "") { throw new Exception(why); }
  parcel.Status = ParcelStatus.SentBack;
}

/// Not one of the moves, so its write is refused.
void Shortcut(Parcel parcel) { parcel.Status = ParcelStatus.Delivered; }

[TestFixture]
void Depot() {
  new Clerk { Name = "Ida" };
  new Parcel { Reference = "P-1" };
}

principal Ida => Clerk.Single(c => c.Name == "Ida");

[Test(Depot)]
[runas(Ida)]
void A_parcel_changes_status_only_through_its_moves() {
  var parcel = Parcel.Single(p => p.Reference == "P-1");
  Assert.Denied(() => Shortcut(parcel));
  Ship(parcel, "TRK-1");
  Deliver(parcel);
  UnitOfWork.Commit();
  Assert.Equal(ParcelStatus.Delivered, Parcel.Single(p => p.Reference == "P-1").Status);
}

[Test(Depot)]
[runas(Ida)]
void A_move_from_the_wrong_state_is_refused() {
  var parcel = Parcel.Single(p => p.Reference == "P-1");
  Assert.Throws(() => Deliver(parcel));               // not shipped yet
}
```

The shop kit's `OrderLifecycle` (`osy source OrderLifecycle`) is a full-size example of the workflow shape: payment
by webhook, a hold that lapses, picking, packing and refunds, all on one tracked `Order.Status`.

## See also       {#see-also}
- [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) — `Tracks` and `Initial`: handing the field to the workflow
- [Transitions — where this item may go next](https://osysharp.com/reference/workflow/transitions/) — the moves a row can make now, for a screen's buttons
- [Raising a workflow event](https://osysharp.com/reference/workflow/raise/) — making a move from code
- [deny … except via](https://osysharp.com/reference/security/except-via/) — naming the functions that may change a guarded field
- [Enum.Parse<T> and Enum.TryParse<T> — text into a member](https://osysharp.com/reference/enum/parse/) — turning text into a member, refusing an unknown name
- [Workflows (the run that outlives the request)](https://osysharp.com/reference/workflow/index/) — what a workflow is, and the first-day mistakes
