Summary#
Two reads answer "how long did this take?" without declaring a deadline of any kind:
var flow = Delivery.For(ticket).Flow; // Lead / Touch / Wait
var where = Delivery.For(ticket).TimeInStates; // where the lead time went, longest firstFlow.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#
<Workflow>.For(entity).Flow // FlowMetrics
<Workflow>.For(entity).TimeInStates // List<StateTime>, longest firstFlowMetrics — Lead, Touch, Wait (all TimeSpan), IsFinished (bool), StartedAt (DateTime),
FinishedAt (DateTime?).
StateTime — State (string), Total (TimeSpan), Visits (int), Accrues (bool).
Description#
Declaring what counts as "working"#
Touch is the time the run spent in the states the workflow lists in Accrues:
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#
A slot's SLA Elapsed — the "1h32m of the 4h" an 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#
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#
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#
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#
A ticket that was worked, blocked on its reporter, then worked again:
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#
- ServiceHours (SLA-accrual windows) —
Accruesand the business-hours calendar, on the SLA side - Assigned / Finished (milestones) — deadlines and breaches, which are a promise rather than a measurement
- For(entity).Audit — the raw timeline these are derived from