# Assigned / Finished (milestones)

> A milestone puts an SLA on a slot's progress — Assigned (someone must PICK IT UP within Within) and Finished (it must be SATISFIED within Within). Each carries a breach arm — Unassigned / Unfinished — that runs when the SLA lapses, and inside that arm the ambient `slot` is the slot the milestone hangs off: `slot.Assign(p)`, `slot.Candidates(u)`, `slot.Assignee`.

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

## Summary        {#summary}
A **milestone** puts a deadline on a slot's progress. There are two, by the two things that can be late:
`Assigned` — the slot must be **picked up** (claimed or pre-assigned) within `Within` — and `Finished` — the slot
must be **satisfied** (deposited into) within `Within`. Each has a **breach arm** that runs when its SLA lapses:
`Unassigned { … }` for Assigned, `Unfinished { … }` for Finished. A breach is **not** a failure by itself: an arm
with no `goto` runs its body and the wait lives on; an arm that `goto`s ends the wait.

## Signature      {#signature}
```osy syntax
Assigned {
  Within = <TimeSpan>;         // must be picked up within this
  Unassigned { <body> }        // ran if still Unassigned at Within (may `goto`)
}
Finished {
  Within = <TimeSpan>;         // must be satisfied within this
  Unfinished { <body> }        // ran if not Satisfied at Within (may `goto`)
}
```

## Description    {#description}
A milestone is declared where its SLA belongs — on a **slot** (inside its `subscribe … { }`), on a **state**, or on
the **workflow** as a cascade default; nearest declaration wins (slot over state over workflow). A cascade default is
inherited only by **pool slots** — a slot that declares `Candidates` — since the SLA models pool pickup (Assigned) and
completion (Finished) and its body reads `slot.Candidates`; a bare system-event slot (`subscribe Submit();`) is not
SLA-tracked. A slot may override just one clause (e.g. `Finished { Within = TimeSpan.FromDays(2); }`) and inherit the
rest. `Within` is a `TimeSpan` expression over `this.Item`, so different instances can carry different SLAs (snapshot
them in `Start { }`).

When the SLA lapses the milestone **breaches**: its breach arm runs, a `Breached` event is written to the timeline,
and — if the arm `goto`s — the run transitions. A milestone that has already been met never breaches (an Assigned
milestone is met the moment the slot leaves `Unassigned`; a Finished milestone the moment the slot is `Satisfied`), so
a breach that *fixes* the problem (e.g. `slot.Assign(lead)`) also stops it recurring.

**The ambient `slot`.** Inside a milestone body the bare identifier `slot` is the slot the milestone hangs off:

- **`slot.Assign(principal)`** — assign the slot to a principal (it becomes theirs; the wait continues).
- **`slot.Candidates(u)`** — evaluate the slot's own `Candidates` predicate against a principal `u` → a `bool`. It is
  the slot's eligibility rule, reusable in a query — `Person.Single(u => slot.Candidates(u) && u.IsLead)` finds the
  lead *among the slot's candidates* as one query.
- **`slot.Assignee` / `slot.Status` / `slot.IsUnassigned`** — read the slot's current holder / state.

**Naming it.** A breach arm may give the slot a name of its own — `Unassigned(Slot approver) { … }` — and then that
name is the slot throughout the body. It is the same one slot either way; naming it just reads better when the body
is about a person rather than a mechanism, and it matches how `on Expire(PoStatus state)` names what is ambient
there. The parameter is a `Slot` and there is exactly one of it.

```osy title="naming the slot so the body reads about a person" syntax
Assigned {
  Within = TimeSpan.FromHours(4);
  Unassigned(Slot approver) {                 // `approver` IS the slot — `slot` is simply its default name
    var lead = Person.Single(u => approver.Candidates(u) && u.IsLead);
    approver.Assign(lead);
  }
}
```

### What if a breach body throws?   {#breach-throws}
Nobody called it, so there is nobody to throw to. What that body changed is not kept, the run still waits, and the
breach is tried again the next time timers run — so a body that always throws leaves the row exactly as it was. The
run's other clocks are not held up by it: a reminder that came due with it fires and is kept.

The failure is recorded on the run, saying which body it was and what it threw, where the app reads it —
`OrderFlow.For(order).Audit`, as an entry whose `Message` begins *The \`Unfinished { }\` body of a milestone threw
when its time came* — and it reaches the platform's operators as a faulted run. `osy test` prints it under a test
that fails afterwards, since a row that "never went overdue" is usually this:

```text
✗ a_request_goes_overdue — at time.test.osy:14:3
  Assertion failed: Assert.NotNull(…) failed: expected a non-null value, but got null

⚠ While this test ran, a workflow body threw with nobody to throw to — …
  · RequestFlow, on Request 4a2f4268-…, in state Open — The `Unfinished { }` body of a milestone threw when its
    time came. Nothing it changed was kept, the run still waits, and it is tried again the next time timers run:
    nobody to send it on to
```

The same holds for `on Deadline` and `on Expire`. A `Remind` body that throws is skipped — its reminder is recorded
with the failure, and the next one is still due ([Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/)).

### `Retries` / `Backoff` / `Exhausted` — try again before giving up   {#retries}
For a slot a MACHINE fills — a child workflow, an external callback — the useful answer to a missed deadline is often
"try again", not "escalate":

```osy title="try again three times before escalating" syntax
Finished {
  Within  = TimeSpan.FromMinutes(5);
  Retries = 3;                          // three further windows
  Backoff = TimeSpan.FromMinutes(1);    // …each one a minute after the last failed
  Exhausted  { slot.Release(); }        // tried and gave up — hand it to the humans
  Unfinished { goto NeedsAttention; }   // …and this is where the run goes
}
```

Each retry re-opens the SAME window (`Within`), delayed by `Backoff`. When the attempts run out, `Exhausted { }` runs
and *then* the breach arm decides where the run goes.

- **A retry does not run the breach arm.** The breach arm may `goto`, so running it per attempt would move the run
  away on the first one and there would be no second attempt. The breach arm is the end of the story, not a step in it.
- **Every attempt is audited**, so the timeline shows three breaches and an exhaustion rather than one long silence.
- **The count is per owner.** A slot that changes hands gives its new holder a fresh set of attempts, for the same
  reason they get a fresh budget: attempts burned by someone else are not a commitment they made.
- **On a HUMAN slot you almost certainly want `Unassigned { … }` instead.** It lets you say *who* to escalate to and
  *when* to give up; `Retries = N` can only re-offer to the same pool on the same terms. It is allowed — three
  re-offers is odd, not wrong — but it is rarely what you meant.

`Backoff` takes either a plain time span — the same wait every time — or a **[retry policy](https://osysharp.com/reference/workflow/backoff/)**, which
says how the wait GROWS and bounds it:

```osy title="a wait that grows, instead of the same wait each time" syntax
Finished {
  Within  = TimeSpan.FromMinutes(5);
  // Three attempts, five minutes apart, then ten — but never more than an hour idle.
  Backoff = Backoff.Exponential(TimeSpan.FromMinutes(5)).MaxAttempts(3).Cap(TimeSpan.FromHours(1));
  Exhausted  { slot.Release(); }
  Unfinished { goto NeedsAttention; }
}
```

⚠ **`Retries = N` and a policy's `.MaxAttempts(M)` are two budgets for one thing, and a milestone declaring both is a
compile error.** They also count differently — `Retries` is how many windows follow the first, `MaxAttempts` is how
many there are in total — so `Retries = 2` and `.MaxAttempts(3)` say the same thing. Pick whichever reads better where
you are; see [Backoff (retry policy)](https://osysharp.com/reference/workflow/backoff/).

### `enter { }` — the success counterpart of the breach arm   {#enter}
A milestone has two outcomes and both have a home. The breach arm runs when the SLA is missed; **`enter { }` runs when
the milestone is REACHED** — the slot got an owner (`Assigned`), or it was satisfied (`Finished`).

```osy title="telling the new owner the moment the slot is taken" syntax
Assigned {
  Within = TimeSpan.FromHours(4);
  enter  { Notify(slot.Assignee); }        // it has an owner now — tell them
  Unassigned { … }                          // …and this is what happens if it never did
}
```

This is the only home for "tell the new owner", and that is why it exists: **claiming a slot changes no workflow
state**, so no state `enter { }` fires. Without it, an approval app has nowhere to put the most ordinary thing it
does.

- **It runs on EVERY assignment, not just the first.** A slot released and re-claimed enters `Assigned` again, and the
  new owner is told — the same reasoning that gives each holder their own `Finished` budget. A hook that fired once
  would go quiet exactly when a slot changes hands, which is when someone most needs to hear.
- **The ambient `slot` is bound**, so `slot.Assignee` is the person who just took it.
- **It may not `goto`.** Reaching a milestone is orthogonal to workflow state: assignment is about WHO, not about
  WHERE the run is. Side effects only — the compiler refuses a `goto` here.
- It is available on `Finished` too (fires when the slot is satisfied), though that is often already covered by the
  `on <Event>` arm that handled the deposit.

## Examples       {#examples}
```osy title="a deadline on getting the slot OWNED" test app=workflow-milestone
using Osysharp.Workflow;

enum Decision   { Approve, Reject }
enum OrderState { Review, Done }

[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 ReviewFlow {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Review;

  event Approve(Decision decision);

  state Review {
    subscribe Approve(Decision decision) as Legal {
      Candidates = u => u.Email != "";
      Assigned {
        Within = TimeSpan.FromHours(4);
        Unassigned { }
      }
    }
    on Approve(Decision decision) { goto Done; }
  }
  terminal success Done { }
}
```

Breach ≠ failure — nobody picked up the pool slot, so on breach we widen it to the department lead and keep waiting:

```osy title="Assigned breach assigns the lead" syntax
subscribe Approve(Decision decision, string reason) as Legal {
  Candidates = u => u.Department == Dept.Legal;
  Assigned {
    Within = TimeSpan.FromHours(4);
    Unassigned {                                   // no goto → keep waiting, now with an owner
      var lead = Person.Single(u => slot.Candidates(u) && u.IsLead);
      slot.Assign(lead);
    }
  }
}
```

A claimed slot that goes overdue *is* fatal — the `goto` ends the wait:

```osy title="Finished breach escalates" syntax
Finished {
  Within = TimeSpan.FromHours(8);
  Unfinished { goto Escalated; }
}
```

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot a milestone hangs off (its `Candidates` / `Assignee`)
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the same `Candidates(u)` question OUTSIDE a milestone, where the run is named rather
  than ambient: `<Wf>.For(item).<Slot>.Candidates(u)`
- [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/) — nudges scheduled off a milestone before it breaches
- [on — a route and its arms](https://osysharp.com/reference/workflow/route/) — the state-level `on … goto …` routes a breach `goto` joins
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state and its `Expire` deadline
