# Flow metrics — how long an item took, and how much was waiting

> Wall-clock lead time for a run, split into the part somebody was working it and the part it sat waiting — plus where that time went, state by state. Derived from what already happened, so it answers for items that finished long before anyone decided to measure.

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

## Summary        {#summary}
Two reads answer "how long did this take?" without declaring a deadline of any kind:

```osy syntax
var flow = Delivery.For(ticket).Flow;            // Lead / Touch / Wait
var where = Delivery.For(ticket).TimeInStates;   // where the lead time went, longest first
```

`Flow.Lead` is wall clock from start to finish. `Flow.Touch` is the part of it the run spent in states you declared
as [[#description|working]]. `Flow.Wait` is the rest — parked on a customer, blocked on a clarification, sitting in a
queue. That split is usually the whole question: *"six days, of which four waiting on the reporter"* says something
*"six days"* does not.

## Signature      {#signature}
```osy syntax
<Workflow>.For(entity).Flow           // FlowMetrics
<Workflow>.For(entity).TimeInStates   // List<StateTime>, longest first
```

**`FlowMetrics`** — `Lead`, `Touch`, `Wait` (all `TimeSpan`), `IsFinished` (`bool`), `StartedAt` (`DateTime`),
`FinishedAt` (`DateTime?`).

**`StateTime`** — `State` (`string`), `Total` (`TimeSpan`), `Visits` (`int`), `Accrues` (`bool`).

## Description    {#description}

### Declaring what counts as "working"   {#accrues}
`Touch` is the time the run spent in the states the workflow lists in `Accrues`:

```osy syntax
Accrues = [Building, InReview];
```

That is the same declaration the SLA clocks read, and deliberately so — "which states are work happening in" is one
fact about your process, not two. A workflow that lists **no** accruing states is ungated, exactly as its clocks would
run around the clock: every state counts, `Touch == Lead`, and `Wait` is zero.

`Touch + Wait == Lead` always holds exactly. `Wait` is derived by subtraction rather than summed from the other
states, so the three numbers can never drift apart by a few ticks and leave a reader wondering which to believe.

### ⚠ This is not an SLA, and it is not the same number   {#vs-sla}
A slot's SLA `Elapsed` — the "1h32m of the 4h" an [inbox](https://osysharp.com/reference/workflow/inbox/) row shows — is a different measurement,
and mixing them up will quietly give you wrong reports:

| | `Flow.Lead` | SLA `Elapsed` |
|---|---|---|
| measures | wall clock | budget consumed |
| under `ServiceHours` | unaffected | advances only in business hours |
| exists when | always | only if a `Within` was declared |
| a Friday 16:00 → Monday 10:00 ticket | **66 hours** | **2 hours** |

Both answers are right. They answer different questions, and only one of them is "how long did the customer wait".

### It works on items that finished before you asked   {#retroactive}
These come from the run's own lifecycle timeline, not from a clock ticking alongside it. So they answer for every run
you have ever executed — including the quarter you now want to report on but were not measuring at the time. That is
the one property a live counter could never acquire afterwards.

⚠ **The limit is your retention window, not the computation.** Reaped runs take their timelines with them, so a year
of delivery history needs a run retention that reaches back a year. Decide that before you need the data — it is not
recoverable later.

### Work still in flight   {#in-flight}
For a run that has not finished, `Lead` is the elapsed time **so far** and `FinishedAt` is null, so a board can show
ageing work. `IsFinished` is what stops a reader taking an in-flight number for a delivered one.

### Rework shows up as `Visits`   {#visits}
`TimeInStates` sums **every** visit to a state, and `Visits` counts them. A state with a four-day total and four
visits is a different process from one with a four-day total and a single visit, and the count is the only thing that
says which you have.

## Examples       {#examples}

A ticket that was worked, blocked on its reporter, then worked again:

```osy title="lead, touch and wait" test app=workflow-flow-metrics
enum TicketState { Building, AwaitingReporter, Shipped }

[Principal] entity Dev {
  [Required, MaxLength(100)] string Name;
  security { allow read when IsAuthenticated; allow create when IsAuthenticated || IsAnonymous; }
}

entity Ticket {
  [Required, MaxLength(200)] string Title;
  TicketState State;
  security { allow read when IsAuthenticated; allow create, update when IsAuthenticated || IsAnonymous; }
}

workflow Delivery {
  Tracks    = Ticket.State;
  Autostart = true;
  Initial   = Building;
  Accrues   = [Building];          // waiting on the reporter is NOT working

  event Ask();
  event Answer();
  event Ship();

  state Building {
    subscribe Ask()  as Blocked;
    subscribe Ship() as Done;
    on Blocked { goto AwaitingReporter; }
    on Done    { goto Shipped; }
  }

  state AwaitingReporter {
    subscribe Answer() as Unblocked;
    on Unblocked { goto Building; }
  }

  terminal success Shipped { }
}

string HowLong(Ticket t) {
  var flow = Delivery.For(t).Flow;
  var worst = Delivery.For(t).TimeInStates.First();
  return $"took {flow.Lead}, worked {flow.Touch}, waited {flow.Wait} — most of it in {worst.State}";
}
```

## See also       {#see-also}
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — `Accrues` and the business-hours calendar, on the SLA side
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — deadlines and breaches, which are a promise rather than a measurement
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the raw timeline these are derived from
