Summary#
The WorkQueues kit's WorkItem is [Authority(Extensible)]: the app derives its kind (Ticket : WorkItem) and states that
kind's grants, and the kit's denies are the floor every grant stands on. The app says three things — who works the
inbox, who leads it, who administers it:
policy WorksInbox => IsAgent;
policy LeadsInbox => IsLead;
policy AdministersInbox => IsAdmin;Signature#
| invariant | what holds |
|---|---|
inbox-a-restricted-queue-is-read-by-its-members-only | an item in a Restricted queue is unreadable to staff outside it — rows and counts; its holder and inbox admins read it, and the requester's read is the app's |
inbox-work-goes-only-to-who-may-hold-it | every path that puts an item on a person — take, hand-on, a routing rule, the queue's distribution, a refill, an account offer, cover, follow-the-sun — asks MayHold: an active member of the item's queue (or on its organisation's account team, when the queue is not restricted) holding every skill it requires |
inbox-assignment-only-through-the-run | holder, queue, priority, escalation level and promise are written only by the kit's runs; every change is a Handover (and every escalation an Escalation) |
inbox-moving-anothers-work-is-a-leads | unless AgentsHandOn, only the holder or a lead moves a held item |
inbox-service-internals-are-staffs | a requester reads no policy, budget, at-risk fraction, escalation level or history, assignment history, offer, clock record, queue or absence |
inbox-a-count-is-the-viewers-own | every count is the viewer's own read, never a privileged aggregate |
inbox-a-view-grants-nothing | a saved view only filters; a personal view is its owner's; shared views are the admins' |
inbox-only-you-or-a-lead-set-your-availability | a person's status, cover, ceiling, hours and absences are set by that person or a lead — nobody marks a colleague away and collects their work |
inbox-cover-grants-no-access | cover moves an item only to somebody who may hold it — a restricted item, or one needing a skill the cover lacks, goes back to its queue; ActsFor is asked beside the app's own rule, so a delegate never acts on their own request |
inbox-service-levels-are-the-admins | queues (their distribution included), SLA policies and targets, routing rules, skills and shared views change only by an inbox admin |
Description#
The app grants; the kit's floor holds#
Grants are never inherited, so a Ticket : WorkItem states its own reads and writes — staff work every ticket, a
customer reads their own. Whatever it grants, the kit's denies hold: an app that grants its agents every ticket still
cannot show one in a restricted queue to an agent outside it, and an app that grants update cannot write
Assignee, Queue, Priority or the promise. Those change only through the kit's verbs, which raise events on the
item's WorkHandling run.
The lead's verbs are their own events#
TakeWork, HandWorkTo and MoveWork raise an agent's event or a lead's, chosen by the caller's LeadsInbox; the
lead's events are gated [Authorize(u => LeadsInbox)], so an agent cannot reach them by raising them herself. With
AgentsHandOn off, an agent may take only unheld work and hand on only what she holds. With it on (the default), any
agent may — the target must still be able to hold it.
Routing gives work only to who may hold it#
A routing rule's AssignTo, a queue's round robin or least loaded, and a refill all end in the same question the verbs
ask. A rule naming somebody who may not hold the item gives it to nobody, and the item's timeline says so; a queue
never gives an item to a member who lacks a skill it requires. Who holds which skills is a lead's to say
(HeldSkill); which skills exist, which rules route and how a queue hands its work out are an inbox admin's.
Cover is not a way in#
When somebody goes away, each item they hold goes to their cover only when the cover may hold it; otherwise it goes
back to its queue. So naming a colleague as cover never hands them a restricted queue's item, or work needing a skill
they lack — and never into a read they did not have. Delegation reads ActsFor(u, p) ("u is p, or covers for p while
p is away"), and it is always asked beside the app's own rule: an approver's delegate may approve, but never their own
request (&& u != this.Item.Requester). The cover ends with the absence.
The collision sentence#
A take or hand-on carries expected — the holder the page showed. If somebody else holds the item now, the verb is
refused with the sentence naming them ("Rahul took this just now."), and nothing moves. [ConcurrencyCheck] on
WorkItem turns a truly simultaneous double-take into one success and one refusal instead of a lost update.
Known limit#
"Only the kit's run writes the holder" means any workflow body today: an app's own lifecycle body writes as the
system too, so it could set Assignee past the kit's deny. Scoping that exemption to the package that declared the
deny is an open platform decision (D-INBOX1, with D-WFRUNAS1).
An absolute due date#
WorkItem.DueAt is a promise no business-hours budget can say — "one calendar month from receipt". An app sets it when
it creates the item; when set, it IS the resolution promise: counted in real time from ArrivedAt, never paused by a
wait, at risk at AtRiskAt of that time and breached at the instant, recorded like any clock, and what the list counts
down to while the resolution governs. It moves only through MoveDue(item, due, why), which is recorded on the item's
trail. The Privacy kit's requests are the first user.
Examples#
A restricted queue, and the verbs that move work#
app SecureDesk {
use Osysharp.Identity@0;
use Osysharp.Organizations@0;
use Osysharp.Organizations.ServicePlans@0;
use Osysharp.Presence@0;
use Osysharp.Observability;
use Osysharp.WorkQueues@1;
use Osysharp.Workflow;
use Osysharp.ServiceHours;
model "model/**/*.osy";
}
using Osysharp.Identity;
using Osysharp.WorkQueues;
[Role] enum DeskRole { Authenticator, Agent, Lead, Admin }
policy WorksInbox => RoleGrant.Any(g => g.Member == user
&& (g.Role == DeskRole.Agent || g.Role == DeskRole.Lead || g.Role == DeskRole.Admin));
policy LeadsInbox => RoleGrant.Any(g => g.Member == user && (g.Role == DeskRole.Lead || g.Role == DeskRole.Admin));
policy AdministersInbox => RoleGrant.Any(g => g.Member == user && g.Role == DeskRole.Admin);
policy ManagesOrganizations => AdministersInbox;
policy CollaboratesOnRecords => WorksInbox;
/// The app grants its agents EVERY incident — and the kit's floor still keeps the Security queue's to its members.
entity Incident : WorkItem {
security { allow read, create, update when WorksInbox; }
}
/// Open an incident straight into the restricted queue.
public Incident Report(string title) {
return new Incident { Title = title, Queue = QueueNamed("Security") };
}
/// A lead hands it to a member of that queue — anyone else is refused, by name.
public void Assign(Incident incident, Principal to) { HandWorkTo(incident, to); }See also#
- The WorkQueues kit — queues, who holds what, and the service levels promised on any item — the model, the clocks, the verbs, views and counts
- [Authority] — why an Extensible package's denies are a floor
- The Organizations kit — companies, teams, and roles held within one — tiers and account teams, read by the kit and never written
kits/work-queues/tests/security.test.osy— one guard per invariant, each red with its guard removed