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

Reference / Workflow

Workflow.Work<T> (everything outstanding) and its SLA numbers

Workflow.Work<T>()

Every live slot of every run tracking T, whoever holds it — the unfiltered sibling of Workflow.Inbox. Rows carry Budget, Elapsed and Remaining for the deadline that governs them, so a screen can say "1h32m of the 4h for first response". One row type serves every viewpoint: an operator reads it whole, a requester filters to their own items, a holder filters on Assignee.

stable1 example compiled by CIworkflowauthoring

Summary#

Workflow.Inbox<T>() answers what is waiting for me. Workflow.Work<T>() answers what is outstanding — every live slot of every run tracking T, whoever holds it.

They return the same row, and that is the point: an operator, the person who raised the item, and the pool that can pick it up all want the same facts, filtered differently.

Signature#

Workflow.Work<TrackedEntity>()

No arguments. Filter, order and count it like any other list.

Description#

How do I get MY queue out of it?#

The inbox has a viewpoint built in — assigned to you, or you satisfy the slot's Candidates. That is what stops it generalising: "where has my expense report got to" is asked by someone who often cannot act on it at all. So the primitive is the unfiltered list, and each viewpoint is an ordinary .Where(…):

Workflow.Work<Expense>()                                        // an operations board — everything
Workflow.Work<Expense>().Where(r => r.Assignee == me)           // what I am holding
Workflow.Work<Expense>().Where(r => r.SlaKind == SlaKind.Assigned && r.Remaining < TimeSpan.Zero)  // late to respond

Workflow.Work<Expense>()                                        // my submissions, wherever they are
  .Include(r => r.Item).Include(r => r.Item.Requester)          // …filtering THROUGH Item needs it loaded
  .Where(r => r.Item.Requester == me)

Filtering through Item needs Item included. The rows are already materialised, so a .Where(…) over them runs in memory — an un-included reference has nothing to resolve. Include the hop you filter on and Item itself. Fields on the row (Assignee, SlaKind, Remaining) need nothing.

Security is not what changed. Dropping the inbox filter drops a relevance question, not a permission one: whether you may see an item at all is decided by that entity's own declared read rules, on the same rows, either way. A requester who can only read their own expenses sees only their own — with no filter written.

Which SLA numbers does a row carry?#

memberwhat it is
Budgetthe SLA's total allowance — the 4h
Elapsedhow much is gone — the 1h32m
RemainingBudget - Elapsed, negative once breached
SlaKindwhich deadline these describe — Assigned (first response) or Finished (completion)
BreachesAtwhen it runs out
foreach (var r in Workflow.Work<Ticket>().OrderBy(r => r.Remaining)) {
  Log.Information($"{r.Item.Title}: {r.Elapsed} of {r.Budget} ({r.SlaKind})");
}

SlaKind is not decoration. A slot can carry both an Assigned and a Finished milestonepick it up within 4h and close it within 24h are different promises. The row describes the one that breaches soonest, which is the same clock BreachesAt reports, so a row is always about one deadline rather than a blend of two. Without SlaKind, "1h32m of 4h" would not say which promise it measures.

Remaining goes negative on purpose. How far past is the thing an operator is looking for, and clamping at zero would flatten the worst rows into the merely-due ones.

All five are null together when no clock governs the slot — absence, not a zero that would sort as though the budget were spent.

Elapsed is accrued, not wall-clock#

Under ServiceHours an SLA only advances during business hours. Elapsed counts the same way, so a ticket raised on Friday afternoon does not burn its budget over the weekend — and Elapsed, Remaining and BreachesAt on one row always agree with each other. A clock declared Accrues = false measures real time, and its Elapsed follows it.

This is also why the numbers come from here rather than being computed in app code: the answer depends on the schedule the run is governed by, which is not a subtraction anyone can do from the outside.

Examples#

A support queue with a 4h first-response SLA and a 24h close, and the two reads an operations screen makes of it.

enum TicketStage { Open, Working, Closed }

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

entity Ticket {
  [Required] [MaxLength(120)] string Title;
  [Required] Agent Reporter;
  TicketStage Stage;
  security {
    allow read, update when IsAuthenticated;
    allow create       when IsAuthenticated;
  }
}

workflow TicketFlow {
  Tracks    = Ticket.Stage;
  Autostart = true;
  Initial   = Open;

  event Pick();
  event Resolve(bool fixed);

  state Open {
    subscribe Pick();
    on Pick { goto Working; }
  }

  state Working {
    subscribe Resolve(bool fixed) as Support {
      Candidates = u => u.Team == "Support";
      Assigned { Within = TimeSpan.FromHours(4);  }   // first response
      Finished { Within = TimeSpan.FromHours(24); }   // close
    }
    on Support(bool fixed) { goto Closed; }
  }

  terminal success Closed { }
}

// Everything past its deadline, worst first — the operator's screen.
int OverdueCount() {
  return Workflow.Work<Ticket>()
                 .Where(r => r.Remaining < TimeSpan.Zero)
                 .Count();
}

// The same read, one viewpoint narrower: where my own tickets have got to. Both Includes are needed — the filter
// navigates Item AND Item.Reporter, and an in-memory Where cannot resolve a reference that was never loaded.
int MySubmissions(Agent me) {
  return Workflow.Work<Ticket>()
                 .Include(r => r.Item)
                 .Include(r => r.Item.Reporter)
                 .Where(r => r.Item.Reporter == me)
                 .Count();
}

See also#

Related

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…

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…

ServiceHours (SLA-accrual windows)

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

Workflow.Retarget (re-base the SLA clocks)

Re-evaluates every SLA clock's budget on the current run against the now-updated entity, so a mid-run change to the SLA…