Summary#
var a = OpenAssignment(new NewAssignment {
Title = "Press P-200 faults intermittently",
Goal = "The press runs a full shift without faulting.",
Criteria = ["The cause is found", "The press ran clean for a shift"],
});
MeetCriterion(a.Criteria.Single(c => c.Position == 1), "A worn pilot valve", evidence: log.Id, evidenceType: "ServiceLog");
CompleteAssignment(a, "The valve was replaced."); // CriteriaUnmet — one criterion is not met, and it says whichSignature#
Assignment OpenAssignment(NewAssignment ask) // Title, Goal, Scope, Criteria, Owner, Assignees, DueAt,
// StalledAfter, Budget, Parent, ParentCriterion, Process
AssignmentCriterion AddCriterion(Assignment a, string text, string key = "", Assignment? metWhen = null)
void MeetCriterion(AssignmentCriterion c, string metBy, Guid? evidence = null, string evidenceType = "")
void ReopenCriterion(AssignmentCriterion c, string why)
void AcceptException(AssignmentCriterion c, string why) // policy AcceptsAssignmentExceptions
Completion CompletionOf(Assignment a) // Ready, ByException, Unmet, Why
void CompleteAssignment(Assignment a, string outcome, string limitations = "", string obligations = "")
void CancelAssignment(Assignment a, string why)
void FailAssignment(Assignment a, string why, string limitations = "")
void ReopenAssignment(Assignment a, string why, List<AssignmentCriterion>? noLongerMet = null)
AssignmentWork AttachWork(Assignment a, WorkAttachment what) // Kind, Key, Title, Subject, WaitingOn, DueAt, Branch
void CloseWork(AssignmentWork work, WorkState state, string result = "")
void BlockWork(AssignmentWork work, string why) · UnblockWork(work) · CountWorkCost(work, totalMicros)
AssignmentWork? DoneFor(Assignment a, string key) · RecordDone(a, key, title, result)
void NoteProgress(Assignment a, string what, Guid reference, string referenceType, string branch = "")
void NoteActivity(Assignment a, ActivityKind kind, string what) // Note, Reminder, Retry, StatusSummary
AssignmentConditions ConditionsOf(Assignment a) // Waiting, Blocked, Overdue, Stalled + why
AssignmentLink LinkAssignments(Assignment from, Assignment to, LinkKind kind, string why = "")
LlmCall ChargedTo(Assignment a, string purpose = "") · AssignmentCost CostOf(Assignment a)
policy OpensAssignments · OverseesAssignments · AcceptsAssignmentExceptions // the app answers each
policy ErasesAssignments // nobody, until the app says
void TakePersonOffAssignment(Assignment a, Principal person, string standIn, string why)
void AnonymiseAssignment(Assignment a, AssignmentScrub how) // Words, StandIn, Blank, Why — a closed assignment
int EraseWordsFromActivity(Assignment a, List<string> words, string standIn, string why)
List<Assignment> AssignmentsOf(Principal person) · bool MayEraseAssignments() · string FormerPersonWords()Description#
When is an assignment done?#
When every criterion is met or excepted — and it has at least one. A criterion is met with what met it: words, and
optionally the id of the evidence or decision it rests on. Finishing every piece of attached work completes nothing.
CompleteAssignment refuses with CriteriaUnmet, naming each criterion in the way. A criterion that will not be met
is settled by AcceptException, which only somebody the app's AcceptsAssignmentExceptions policy admits may call;
the assignment then closes as CompletedByException.
What happens when the outcome does not hold?#
ReopenAssignment(a, why, noLongerMet) opens the same assignment again. It keeps its criteria, its activity, what
is attached to it and what it cost; the criteria named go back to unmet (with none named, every one that was met by
hand does). Whoever asked, whoever owns it and whoever oversees assignments may.
What counts as progress?#
Something established: a criterion met, a piece of work done, an exception accepted, or NoteProgress — which
refuses a note that points at nothing. A reminder, a retry, a status summary and a plain note are activity
(NoteActivity). The assignment keeps LastActivityAt and LastProgressAt apart, and BranchesOf(a) keeps both
per parallel branch.
How do I know why it is not moving?#
ConditionsOf(a) works out four conditions, each with its reasons. More than one can hold; a closed assignment has
none.
| condition | holds when |
|---|---|
Waiting | open work is pending, inside the time allowed for it |
Blocked | a piece of work is blocked, or an assignment it depends on is not completed |
Overdue | the assignment's date, or a piece of work's, has passed |
Stalled | nothing has been established for longer than it allows (StalledAfter; the app's default is three days) |
What can be attached to it?#
Anything it waits on or has had done: an agent's piece of work, a process, a request to a person, a handoff, an
action. AttachWork is safe to repeat — the same Key answers the piece of work already there — so a step that is
run again after a crash attaches nothing twice. For something that must not HAPPEN twice, ask DoneFor(a, key)
first and RecordDone in the same unit of work as the doing.
How do assignments relate to each other?#
An assignment opened with Parent is under another; with ParentCriterion its completion meets that criterion on
the parent, and reopening it puts the criterion back in question. Whoever is on an assignment reads what is opened
under it. LinkAssignments(from, to, LinkKind.DependsOn) blocks from until to is completed.
Who may see and act on one?#
Whoever is on it reads it: whoever asked, its owner, and the people — or agents' accounts — put on it with
AddParticipant. Assignees and contributors change it; observers do not. OverseesAssignments reads and acts on
all of them. Its state, criteria, activity, people and attached work are written only by the kit's own functions.
Can a person be taken off it, or its words erased?#
Yes, and the assignment stays — it is the organisation's record of work it did.
TakePersonOffAssignment(a, person, standIn, why)takes somebody who was ON it off: their seat goes; who asked, who owns, who closed and who a piece of work waits on become nobody where it was them; each line they wrote and each criterion they settled is marked as a former person's, and a page shows Somebody no longer here (FormerPersonWords()) — never "The app", and never nothing. Their name and address are replaced bystandInin the assignment's sentences.AssignmentsOf(person)lists every assignment they are on or did something on.AnonymiseAssignment(a, new AssignmentScrub { Words, StandIn, Why })erases what a closed assignment says about the person it is ABOUT: each ofWords— a name, an address, an order number — is replaced byStandIn("a former customer") in its title, goal and scope, each criterion and what met it, each piece of work and its result, every line of activity and how it ended. WithBlank = trueevery sentence is replaced whole. Either way what happened stays: each criterion and whether it was met, each line's kind and time, the cost, how it closed.EraseWordsFromActivity(a, words, standIn, why)replaces words in the activity alone, on an open assignment too — what the repositories kit calls when an item whose title the activity named is erased.
Who may: a person the app's ErasesAssignments policy admits — nobody, until the app says. Whoever asked for an
assignment, whoever owns it and whoever oversees every assignment may not. An app that uses Osysharp.Privacy
writes none of this: Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request does it for a person who asks, and when a period ends.
What does it cost?#
ChargedTo(a) is an LlmCall whose scope is the assignment and whose limit is what is left of its budget; a call
that does not fit is refused before anything is sent. CostOf(a) adds its own calls, what attached work reported
(CountWorkCost) and what the assignments under it spent.
Agent work given with AssignedWork.Do carries that same call: every model call it makes is charged to the
assignment as it returns and held to what the assignment has left, so work that would pass the budget is stopped
while it runs. Its receipt is on the assignment before it starts, so CancelAssignment calls it off. When the work
ends, its share is reported as the work's (CountWorkCost(work, total, chargedAsSpent: true)) and is counted once.
Work the agent hands on is the assignment's too: each piece is attached beside the piece that handed it on ("Resolver: investigate delivery — handed on by CaseLead"), for every specialist that is on the assignment, and settled when it ends. The whole tree is charged to the assignment as it spends; the piece at the top counts all of it, and a piece that was handed on counts nothing itself — its own figure is on its receipt.
Examples#
app Workshop {
use Osysharp.Assignments;
use Osysharp.Llm;
use Osysharp.Identity;
use Osysharp.Observability;
use Osysharp.Ui;
model "model/**/*.osy";
}
using Osysharp.Assignments;
using Osysharp.Identity;
[Role] enum ShopRole { Authenticator, Desk, Technician, Manager }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsDesk => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Desk);
policy IsManager => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Manager);
// The kit's questions, answered by this app.
policy OpensAssignments => IsDesk || IsManager;
policy OverseesAssignments => IsManager;
policy AcceptsAssignmentExceptions => IsManager;
entity Person : Principal {
security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
[TestFixture]
void Shop() {
var dana = new Person { Email = "[email protected]", DisplayName = "Dana" };
var tariq = new Person { Email = "[email protected]", DisplayName = "Tariq" };
var mona = new Person { Email = "[email protected]", DisplayName = "Mona" };
new RoleGrant { Member = dana, Role = ShopRole.Desk };
new RoleGrant { Member = tariq, Role = ShopRole.Technician };
new RoleGrant { Member = mona, Role = ShopRole.Manager };
}
principal Dana => Person.Single(p => p.Email == "[email protected]");
principal Tariq => Person.Single(p => p.Email == "[email protected]");
principal Mona => Person.Single(p => p.Email == "[email protected]");
Assignment Job() { return Assignment.Single(); }
AssignmentCriterion Criterion(int position) { return Job().Criteria.Single(c => c.Position == position); }
[Test(Shop)]
void An_assignment_completes_by_its_criteria_and_is_opened_again_with_what_it_had() {
TestClock.Set(new DateTime(2026, 5, 4, 9, 0, 0));
runas (Dana) {
OpenAssignment(new NewAssignment {
Title = "Press P-200 faults intermittently",
Goal = "The press runs a full shift without faulting.",
Criteria = ["The cause is found", "The customer approved the cost", "The press ran clean for a shift"],
Assignees = [Tariq],
});
}
runas (Tariq) {
// A visit is attached, blocked, and then done. None of that completes anything.
var visit = AttachWork(Job(), new WorkAttachment { Key = "visit-1", Title = "First visit", WaitingOn = Tariq });
BlockWork(visit, "The site is closed.");
Assert.Equal(["Blocked"], ConditionsOf(Job()).Names());
UnblockWork(visit);
CloseWork(visit, WorkState.Done, "Valve replaced.");
MeetCriterion(Criterion(1), "A worn pilot valve");
MeetCriterion(Criterion(3), "Eight hours with no fault");
var refused = Assert.Throws<CriteriaUnmet>(() => CompleteAssignment(Job(), "Fixed."));
Assert.Contains("1 of 3 criteria is not met: “The customer approved the cost”.", refused.Message);
// The technician may not wave the approval through.
Assert.Denied(() => AcceptException(Criterion(2), "They always pay."));
}
runas (Mona) { AcceptException(Criterion(2), "Covered by the service contract."); }
runas (Tariq) {
CompleteAssignment(Job(), "The valve was replaced.");
Assert.Equal(AssignmentState.CompletedByException, Job().State);
}
// Three days of reminders are activity; they establish nothing.
runas (Dana) { ReopenAssignment(Job(), "The press faulted again.", [Criterion(1), Criterion(3)]); }
TestClock.Advance(TimeSpan.FromDays(4));
runas (Tariq) {
NoteActivity(Job(), ActivityKind.Reminder, "Reminded the customer.");
Assert.Equal(1, Job().Reopened);
Assert.Equal(CriterionState.Excepted, Criterion(2).State); // what still holds was kept
Assert.Equal(WorkState.Done, WorkOf(Job(), "visit-1")!.State); // and so was everything attached to it
Assert.Equal(["Stalled"], ConditionsOf(Job()).Names()); // the reminder moved nothing
Assert.Equal(new DateTime(2026, 5, 4, 9, 0, 0), Job().LastProgressAt);
}
}
[Test(Shop)]
[runas(Tariq)]
void What_was_done_once_is_not_done_again() {
runas (Dana) { OpenAssignment(new NewAssignment { Title = "Order a valve", Goal = "A valve is on the shelf.", Criteria = ["Delivered"], Assignees = [Tariq] }); }
Assert.Null(DoneFor(Job(), "part-order"));
RecordDone(Job(), "part-order", "Order the valve", "Ordered, reference 7731");
// A step that is run again asks first, and finds it.
Assert.Equal("Ordered, reference 7731", DoneFor(Job(), "part-order")!.Result);
Assert.Equal(1, Job().Work.Count());
}See also#
- What an assignment asks of people — requests as an assignment's work and as inbox items (Assignments.WorkQueues kit) — what an assignment asks of people: requests as its work, and in their inboxes
- Work repositories — what an assignment knows, kept as items (WorkRepositories kit) — what an assignment knows, kept as items
- Writing an agent — a class with a purpose, skills and an account of its own (Agents kit) — an agent, whose account can be put on an assignment
- Model spend — budgets, the meter and the price book (Llm kit) — budgets, limits and the meter behind an assignment's cost
- [Actor] — a principal that code may run as — an actor names the assignment as the piece of work its changes are for
- [On] — hear an event, after the commit that raised it — hearing
AssignmentCompleted,AssignmentReopenedand the rest