# Assignments — a piece of work owned until its outcome is reached (Assignments kit)

> An assignment of the `Osysharp.Assignments` kit says who asked, the outcome wanted and what must be true for it to be done. Agent runs, processes, requests to people and work handed to other systems are attached to it; any of them can fail or be replaced, and the assignment stays. It completes only when every criterion is met — or somebody the app trusts accepts a written exception — and can be opened again with everything it had. It tells progress from activity, works out whether it is waiting, blocked, overdue or stalled with the reason for each, and knows what it has cost.

<!-- id: agent-assignments · area: agent · stability: preview · html: https://osysharp.com/reference/agent/assignments/ -->

## Summary        {#summary}
```osy syntax
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 which
```

## Signature      {#signature}
```osy syntax
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    {#description}

### When is an assignment done?   {#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?   {#reopen}
`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?   {#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?   {#conditions}
`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?   {#work}
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?   {#links}
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?   {#who}
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?   {#erasure}
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 by `standIn` in
  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 of `Words` — a name, an address, an order number — is replaced by `StandIn` ("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. With `Blank = true` every 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](https://osysharp.com/reference/agent/work-repositories/) 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](https://osysharp.com/reference/agent/privacy/) does it for a person who asks, and when a period ends.

### What does it cost?   {#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](https://osysharp.com/reference/agent/delegation/) 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       {#examples}
```osy title="criteria, a refusal, an exception, completion and reopening" run app=agent-assignments
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 = "dana@workshop.example", DisplayName = "Dana" };
  var tariq = new Person { Email = "tariq@workshop.example", DisplayName = "Tariq" };
  var mona = new Person { Email = "mona@workshop.example", 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 == "dana@workshop.example");
principal Tariq => Person.Single(p => p.Email == "tariq@workshop.example");
principal Mona  => Person.Single(p => p.Email == "mona@workshop.example");

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       {#see-also}
- [What an assignment asks of people — requests as an assignment's work and as inbox items (Assignments.WorkQueues kit)](https://osysharp.com/reference/agent/assignment-requests/) — 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)](https://osysharp.com/reference/agent/work-repositories/) — what an assignment knows, kept as items
- [Writing an agent — a class with a purpose, skills and an account of its own (Agents kit)](https://osysharp.com/reference/agent/agents/) — an agent, whose account can be put on an assignment
- [Model spend — budgets, the meter and the price book (Llm kit)](https://osysharp.com/reference/agent/llm-spend/) — budgets, limits and the meter behind an assignment's cost
- [[Actor] — a principal that code may run as](https://osysharp.com/reference/security/declared-actor/) — an actor names the assignment as the piece of work its changes are for
- [[On] — hear an event, after the commit that raised it](https://osysharp.com/reference/function/on/) — hearing `AssignmentCompleted`, `AssignmentReopened` and the rest
