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

Reference / Workflow

Workflow.WorkByItem<T> (one row per item — the board read)

Workflow.WorkByItem<T>() // one row per tracked entity with a live run

One row per tracked ENTITY, where `Workflow.Work<T>()` is one per SLOT. A board, a queue and a "my work" screen are all per item: an item with three open waits is one card, and a board built on the slot read shows it three times. The row carries the clock that governs across all of an item's slots, who holds it, how many waits it has, and whether any promise has ever been missed.

stable1 example compiled by CIworkflowqueriessla

Summary#

Workflow.WorkByItem<T>() returns one row per tracked entity that has a live run. Workflow.Work<T>() returns one row per slot, and the difference is the difference between a queue and a board.

An item usually waits on more than one thing at once. A ticket being worked might have a fix slot, a park slot and an escalation slot all open — so a board built straight on the slot read shows that ticket three times. Worse, the duplication passes every "are the tickets there" check you would think to write.

Signature#

Workflow.WorkByItem<Ticket>()      // List<Osyrin.WorkItemRow_Ticket>

Takes no arguments and filters like any query:

Workflow.WorkByItem<Ticket>().Where(r => r.EverBreached).OrderBy(r => r.BreachesAt)

Description#

The row#

memberwhat it is
Itemthe tracked entity, typed — navigable and .Include-able
Assigneewho holds the item, resolved across all its slots
OpenSlotshow many live waits it has
EverBreachedhas any promise on this run ever been missed
SlaKind · Budget · Elapsed · Remaining · BreachesAtthe governing clock across the item's slots
GoverningSlotwhich slot PROMISED those numbers — null when the run's own clock governs (see below)
RunId · WorkflowNamethe run driving it

The holder is resolved across ALL slots#

A card asks "who has this ticket", and any slot somebody holds answers it — the person who took the reply slot is working the ticket whether or not that slot is the one breaching soonest.

This is worth stating because the obvious shortcut is wrong in a way that is hard to see. Slots of one run routinely share a breach instant — a state's Expire governs several of them, each computed separately — so "the governing slot" is not a stable choice between them. An app that read the holder off the governing row got a card saying "unassigned" while its own roster said the ticket was held.

The governing clock, and how ties break#

The SLA members describe the promise breaching soonest across the item's slots. Ties are the normal case, not an edge one, so the rule is fixed and stated rather than left to each app to discover:

  1. soonest BreachesAt;
  2. then a slot whose clock is its own rather than the run's;
  3. then a slot that somebody holds;
  4. then the slot's declaration order.

A slot with no promise of its own borrows the run's#

A slot that declares no milestone still reports an SLA — the run's: the state's Expire, or the instance Deadline. That is deliberate, and it is why those two SlaKind members exist: a ticket waiting in a state whose promise is ticking should say so, not report nothing.

But a borrowed clock is not that slot's promise, and every promise-less slot on a run borrows the SAME one. So rule 2 exists: choosing between them by an instant they all share is choosing arbitrarily, and before it existed the arbiter was declaration order — which a workflow-scope subscribe wins forever, because it is declared before any state's slots. An always-open escalation hatch became what the board counted down, what "take it" claimed, and what every verb on the page was aimed at, while the reply the item was actually waiting for sat untouched. Nothing errored anywhere.

GoverningSlot is null when the run's own clock governs#

When the winning clock is the run's, no slot is named — because none promised it. Naming one would say "this countdown is counting Bump" when it is counting the state's expiry, which Bump merely stands next to. SlaKind says which run promise it is, so a screen can still say what it is counting.

This is also the difference between a wrong answer and a visible one. An app reading GoverningSlot as a claim target now gets a null it has to handle, rather than a slot the caller cannot claim and a refusal two steps later. Claiming is a different question — "which row may I take?" — and wants Workflow.Work<T>(), which still reports the borrowed clock per slot exactly as before.

An item with no live clock still appears#

A parked, blocked or finished item is still on the board. It comes back with every SLA member null — absence, not a zero, which would sort as a fully-consumed budget and put the calmest rows where the worst ones belong.

EverBreached survives the clock being retired#

This is the member that makes a board correct, and the reason is not obvious. When a promise runs out the engine stamps the breach and retires the clock — so the item stops having a countdown at all. A board ordered by "has a live promise" therefore drops the one row everybody needs to see, at the exact moment it starts mattering, and sorts breached work to the bottom.

EverBreached is read from the workflow's own audit trail, so it is still true afterwards. An app does not need to keep its own flag for this.

…but it does not survive the RUN ending

The clock and the run are two different horizons, and this member only outlives the first. WorkByItem returns one row per live run — so when a breach arm ends the run (Unfinished { goto Expired; }, the ordinary shape for a deadline that actually means something) the row disappears, and EverBreached goes with it. The board asks "was this ever late?" in the same sweep that made the answer true, and gets nothing back at all.

That is not a bug in the read: a finished item has no live work, which is exactly what this read is for. It is a reason to ask a different source. The audit trail has no such horizon — it is where EverBreached came from in the first place, and it is still there when the run is over:

foreach (var a in Onboarding.For(i).Audit) {
  if (a.Kind == AuditKind.Breached) { everBreached = true; }
  if (a.Kind == AuditKind.Reminded) { nudges = nudges + 1; }
}

So: read EverBreached off the row when the breach leaves the run open (a support ticket still owed an answer — the case this member was built for), and off the trail when the breach ends it. A board that mixes finished and unfinished items wants the trail, because only it answers for both.

Examples#

A whole desk, compiled: three waits open on one ticket, and a board that shows it once.

enum Stage { Working, Done }

[Principal] entity Agent {
  [Required, MaxLength(80)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Ticket {
  [Required, MaxLength(120)] string Subject;
  Stage State;
  security { allow read, create, update when IsAuthenticated; }
}

workflow TicketFlow {
  Tracks    = Ticket.State;
  Autostart = true;
  Initial   = Working;

  event Fix();
  event Park();
  event Escalate();

  // Three things can happen to a ticket being worked. On a SLOT board that is three rows; here it is one card
  // that knows it is waiting on three things.
  state Working {
    subscribe Fix()      as FixIt  { }
    subscribe Park()     as Parked { }
    subscribe Escalate() as Raised { }
    on Complete { goto Done; }
  }

  terminal success Done { }
}

// The board: one row per ticket, worst first. `EverBreached` leads because a breach retires the clock, so ordering
// on the live figures alone sends the worst row to the bottom.
List<Osyrin.WorkItemRow_Ticket> Board() {
  return Workflow.WorkByItem<Ticket>()
    .Include(r => r.Item)
    .OrderBy(r => r.EverBreached ? 0 : 1)
    .ThenBy(r => r.BreachesAt)
    .ToList();
}

// "Waiting on more than one thing" — a question the slot board cannot ask at all.
int Stalled() { return Workflow.WorkByItem<Ticket>().Where(r => r.OpenSlots > 1).Count(); }

Two more questions the same row answers. "What am I working on":

Workflow.WorkByItem<Ticket>().Where(r => r.Assignee == Session.CurrentUser.Id)

"Waiting on more than one thing":

Workflow.WorkByItem<Ticket>().Where(r => r.OpenSlots > 1)

See also#

Related

Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers

Every live slot of every run tracking T, whoever holds it — the unfiltered sibling of Workflow.Inbox. Rows carry…

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…

Transitions — where this item may go next

One row per move this instance can make right now, with each arm's guard evaluated against it. A board offers only the…

ServiceHours (SLA-accrual windows)

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

Remind (milestone reminders)

A reminder scheduled off a milestone. Its SCHEDULE is config in the header parens — `After` is the first fire (once, at…