# 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);
  }
}
```

### `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
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
- <span class="planned" title="this page is planned and not written yet">workflow-route</span> — 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
