Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Workflow

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

<Workflow>.For(entity).Flow · <Workflow>.For(entity).TimeInStates

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.

stable1 example compiled by CIworkflowreporting

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 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#

<Workflow>.For(entity).Flow           // FlowMetrics
<Workflow>.For(entity).TimeInStates   // List<StateTime>, longest first

FlowMetricsLead, Touch, Wait (all TimeSpan), IsFinished (bool), StartedAt (DateTime), FinishedAt (DateTime?).

StateTimeState (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.LeadSLA Elapsed
measureswall clockbudget consumed
under ServiceHoursunaffectedadvances only in business hours
exists whenalwaysonly if a Within was declared
a Friday 16:00 → Monday 10:00 ticket66 hours2 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#

Related

ServiceHours (SLA-accrual windows)

A schedule the SLA clock accrues within — the platform WALKS its weekly windows (and holiday exceptions) to advance…

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…

For(entity).Audit

Reads a running instance's lifecycle timeline — every transition, claim, deposit, reminder and refusal as an…

Workflow.Inbox&lt;T&gt; (what is waiting for me)

The current principal's queue: every slot they can act on, across every run of every workflow that tracks T. Rows carry…