# What an assignment asks of people — requests as an assignment's work and as inbox items (Assignments.WorkQueues kit)

> With the Assignments kit and the HumanRequests kit both in an app, a request put for an assignment is a piece of the assignment's work — who the next step is with, when it is wanted by, how it ended — and an item in the inbox of whoever it waits on, whether or not they are on the assignment. What an agent proposes or asks while it works for the assignment is attached by itself. An answer closes the work and completes nothing; a missed date and a reminder are on the assignment's trail. The assignment's page lists who it is waiting on, and the inbox item says which assignment it is part of.

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

## Summary        {#summary}
```osy syntax
app Pressworks {
  use Osysharp.Assignments;
  use Osysharp.HumanRequests;
  use Osysharp.Assignments.WorkQueues;     // a request put for an assignment is a piece of its work
}

var request = AssignmentRequests.Ask(job, new NewRequest {
  Kind = RequestKind.Approval, To = customer, Title = "Approve the cost of the repair?",
  About = Subject.Of("RepairQuote", quote.Id, "Quote 114", version: "r" + quote.Revision),
}, key: "quote");
```

## Signature      {#signature}
```osy syntax
class AssignmentRequests {
  static HumanRequest Ask(Assignment a, NewRequest ask, string key = "", string branch = "");
  static List<HumanRequest> Open(Assignment a);       // its open requests, the oldest first
  static Assignment? Of(HumanRequest request);        // the assignment a request is part of
}
AssignmentRequestIds(Guid assignment) → List<Guid>    // for a page: the open requests' ids
AssignmentTagOf(Guid request) → AssignmentTag?        // for the inbox item: Title, Goal, Progress, State

// drawn for you: "Waiting on people" on every assignment's page · "Part of …" on the request's inbox item
```

## Description    {#description}

### What does `Ask` add to `PutRequest`?   {#ask}
The same [request](https://osysharp.com/reference/agent/human-requests/) — a question, an approval of one version, a form — put to the same
person, in the same inbox. And on the assignment a piece of [attached work](https://osysharp.com/reference/agent/assignments/) of kind `Request`
that stands for it: its title, who the next step is with (`WaitingOn`), what is waited for ("their approval"), and
its date. So the assignment's own page, its conditions (`ConditionsOf(a).NextStepWith`) and its trail all say what it
is waiting on, without the app keeping the two in step.

`key` makes asking safe to repeat: asked again under the same key — a process step that is run again — the request
already put is answered, and nothing is put twice.

The person asked need not be on the assignment. A customer finds the request in their inbox and answers it; the
assignment itself stays unreadable to them.

### What happens when the request ends?   {#ends}
| The request | Its piece of work | The assignment |
|---|---|---|
| is answered | `Done` — "Approved by Carl Lind.", "Not approved by …: the reason", "Answered by …" | **nothing is completed**: what done means is its criteria |
| runs out of time | `Failed` — "Nobody answered in the time it could wait." | nothing is waived |
| is called off | `Cancelled`, with why | — |
| is late | stays open; the trail says its date passed, and who it went on to | its conditions say overdue |
| reminds the person | stays open; the trail says a reminder was sent | — |
| is given a new date | its date moves, with why | — |

### What an agent asks while it works for the assignment   {#agents}
Agent work done for an assignment ([Assignments — a piece of work owned until its outcome is reached (Assignments kit)](https://osysharp.com/reference/agent/assignments/), `AssignedWork.Do`) that
[[agent-autonomy#proposal|proposes]] something or [asks a person](https://osysharp.com/reference/agent/people/) stops and waits. With
[`Osysharp.Agents.HumanRequests`](https://osysharp.com/reference/agent/requests/) in the app that proposal or question is a request — and this
bridge attaches it to the same assignment, beside the agent's own piece of work, which stays open until the agent
has finished. Nothing to wire.

### What is on screen?   {#screens}
- **On the assignment's page** (`AssignmentView`): a "Waiting on people" panel — each open request, of whom, since
  when, late or not; and that nobody is being waited on, when nobody is.
- **On the request's item in the inbox**: "Part of *the assignment's title*", the outcome wanted and how far it is
  ("1 of 3 met") — for whoever may read the assignment, and nothing for anybody else.

## Examples       {#examples}
```osy title="a job asks its customer and its technician; the answers close the work and complete nothing" run app=agent-assignment-requests
app Pressworks {
  use Osysharp.Assignments;
  use Osysharp.Assignments.WorkQueues;
  use Osysharp.HumanRequests;
  use Osysharp.WorkQueues;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Organizations;
  use Osysharp.Organizations.ServicePlans;
  use Osysharp.Presence;
  use Osysharp.Observability;
  use Osysharp.Workflow;
  use Osysharp.ServiceHours;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Organizations.ServicePlans;
using Osysharp.WorkQueues;
using Osysharp.HumanRequests;
using Osysharp.Assignments;
using Osysharp.Assignments.WorkQueues;
using Osysharp.Llm;
using Osysharp.Workflow;
using Osysharp.ServiceHours;

app.Llm = new LlmSetup {
  Models = [ new LlmModel { Name = "Small", Client = new ScriptedModel { Name = "Small" }, Model = "scripted-small",
                            Price = new LlmPrice { Input = 1, Output = 5 }, MaxOutputTokens = 1000 } ],
  Default = "Small",
};
app.Inbox = new InboxSetup { DefaultQueue = "Workshop" };

[Role] enum ShopRole { Authenticator, Desk, Technician, Manager, Customer }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsDesk       => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Desk);
policy IsTechnician => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Technician);
policy IsManager    => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Manager);
policy IsCustomer   => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Customer);
policy IsStaff      => IsDesk || IsTechnician || IsManager;

// Each kit's questions, answered.
policy OpensAssignments            => IsDesk || IsManager;
policy OverseesAssignments         => IsManager;
policy AcceptsAssignmentExceptions => IsManager;
policy WorksInbox       => IsStaff || IsCustomer;      // a customer has an inbox too, for what the workshop asks
policy LeadsInbox       => IsManager;
policy AdministersInbox => IsManager;
policy ManagesOrganizations  => IsManager;
policy CollaboratesOnRecords => IsStaff;
policy AsksPeople       => IsStaff;
policy OverseesRequests => IsManager;
policy ReadsModelUsage   => IsManager;
policy ManagesModelSpend => IsManager;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
partial entity ServiceHours { security { allow read when IsStaff; } }
partial entity ServiceWindow { security { allow read when IsStaff; } }
partial entity ServiceException { security { allow read when IsStaff; } }
partial entity WorkItem { security { allow read when IsStaff || IsCustomer; } }

/// Open the job for a press that faults.
public Assignment OpenRepair(Person technician) {
  return 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"],
    Assignees = [technician],
  });
}

/// Put the cost to the customer: an approval of this quote as it stands, attached to the job.
public HumanRequest AskCustomerToApprove(Assignment job, Person customer, decimal amount) {
  return AssignmentRequests.Ask(job, new NewRequest {
    Kind = RequestKind.Approval, To = customer, Title = "Approve the cost of the repair?",
    Facts = [new RequestFact("Cost", amount.ToString("0.00") + " EUR")],
    About = Subject.Of("RepairQuote", job.Id, "The quote for the press", version: "r1"),
  }, key: "quote");
}

/// Ask the technician when he can be on site.
public HumanRequest AskForAVisit(Assignment job, Person technician) {
  return AssignmentRequests.Ask(job, new NewRequest {
    Kind = RequestKind.Question, To = technician, Title = "When can you be on site?",
    Options = [RequestOption.Of("This week"), RequestOption.Of("Next week")],
  }, key: "visit");
}

[TestFixture]
void Floor() {
  var dana  = new Person { Email = "dana@pressworks.example",  DisplayName = "Dana Holm" };
  var tariq = new Person { Email = "tariq@pressworks.example", DisplayName = "Tariq Aziz" };
  var mona  = new Person { Email = "mona@pressworks.example",  DisplayName = "Mona Berg" };
  var carl  = new Person { Email = "carl@foundry.example",     DisplayName = "Carl Lind" };
  new RoleGrant { Member = dana,  Role = ShopRole.Desk };
  new RoleGrant { Member = tariq, Role = ShopRole.Technician };
  new RoleGrant { Member = mona,  Role = ShopRole.Manager };
  new RoleGrant { Member = carl,  Role = ShopRole.Customer };
  new OrganizationTier { Name = "Standard", Rank = 1, IsDefault = true };
  var queue = new Queue { Name = "Workshop", Position = 1 };
  foreach (var p in new List<Person> { dana, tariq, mona, carl }) { new QueueMember { Queue = queue, Member = p }; }
}
principal Dana  => Person.Single(p => p.Email == "dana@pressworks.example");
principal Tariq => Person.Single(p => p.Email == "tariq@pressworks.example");
principal Mona  => Person.Single(p => p.Email == "mona@pressworks.example");
principal Carl  => Person.Single(p => p.Email == "carl@foundry.example");

[Test(Floor)]
void A_jobs_requests_are_in_their_peoples_inboxes_and_on_the_job() {
  Guid job = Guid.Empty;
  runas (Dana) {
    var a = OpenRepair(Tariq);
    UnitOfWork.Commit();
    job = a.Id;
    AskCustomerToApprove(a, Carl, 480m);
    AskForAVisit(a, Tariq);
    // Asked again under the same key, nothing is put twice.
    UnitOfWork.Commit();
    AskForAVisit(a, Tariq);
    Assert.Equal(2, HumanRequest.Count());
  }
  Background.Settle();
  // The customer finds his request in his inbox, without being on the job — which is not his to read.
  runas (Carl) {
    var mine = Requests.Mine().Single();
    Assert.Equal("Approve the cost of the repair?", mine.Title);
    Assert.Equal(0, Assignment.Count());
    Assert.Null(AssignmentTagOf(mine.Id));
  }
  // The technician finds his — and, being on the job, which job it is part of.
  runas (Tariq) {
    var tag = AssignmentTagOf(Requests.Mine().Single().Id)!;
    Assert.Equal("Press P-200 faults intermittently", tag.Title);
    Assert.Equal("0 of 2 met", tag.Progress);
  }
  // The manager sees both on the job: open work, with who each waits on.
  runas (Mona) {
    var a = Assignment.Single(x => x.Id == job);
    var open = AssignmentWork.Where(w => w.Assignment == a && w.State == WorkState.Open).OrderBy(w => w.Number).ToList();
    Assert.Equal(["Approve the cost of the repair?", "When can you be on site?"], open.Select(w => w.Title).ToList());
    Assert.Equal(["Carl Lind", "Tariq Aziz"], open.Select(w => w.WaitingOn!.DisplayName).ToList());
    Assert.Equal(["Carl Lind", "Tariq Aziz"], ConditionsOf(a).NextStepWith);
    Assert.Equal(2, AssignmentRequests.Open(a).Count);
  }

  // They answer. Each piece of work is done — and the job is not: none of what done means has been met.
  runas (Carl) { AnswerRequest(Requests.Mine().Single(), RequestAnswer.Approve()); }
  runas (Tariq) { AnswerRequest(Requests.Mine().Single(), RequestAnswer.Choose("next-week")); }
  Background.Settle();
  runas (Mona) {
    var a = Assignment.Single(x => x.Id == job);
    var work = AssignmentWork.Where(w => w.Assignment == a).OrderBy(w => w.Number).ToList();
    Assert.Equal(["Approved by Carl Lind.", "Answered by Tariq Aziz."], work.Select(w => w.Result).ToList());
    Assert.Equal(0, AssignmentRequests.Open(a).Count);
    Assert.Equal(AssignmentState.Open, a.State);
    Assert.Throws<CriteriaUnmet>(() => CompleteAssignment(a, "The customer approved."));
  }
}
```

## See also       {#see-also}
- [Assignments — a piece of work owned until its outcome is reached (Assignments kit)](https://osysharp.com/reference/agent/assignments/) — assignments: an outcome that is owned until it is done
- [Requests to people — a question, an approval, a form, and the answer coming back (HumanRequests kit)](https://osysharp.com/reference/agent/human-requests/) — the requests kit: questions, approvals, forms, time
- [An agent's proposals and questions in a person's inbox, and an inbox item in an agent's hands (Agents.HumanRequests kit)](https://osysharp.com/reference/agent/requests/) — an agent's proposals and questions as requests
- [The WorkQueues kit — queues, who holds what, and the service levels promised on any item](https://osysharp.com/reference/workflow/inbox-kit/) — the work queues a request waits in
