# Requests to people — a question, an approval, a form, and the answer coming back (HumanRequests kit)

> The `Osysharp.HumanRequests` kit puts something to a person — a question with the options it may be answered with, an approval of one exact version of a thing, a request for the fields of a class you declare — from a person, a process or an agent, and brings the answer back typed to whatever waits for it, however long that takes. A request is an ordinary item of the app's work queues, answered on its own sheet in the inbox; somebody with no account answers through a link that opens that one request. It keeps time in working hours, reminds up to a limit, goes on to the next person when it is late without deciding anything by the silence, and keeps an answer that comes too late beside the request without applying it. An approval is used once, for the version approved.

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

## Summary        {#summary}
Work stops at people: somebody has to choose, approve, or say what only they know. `PutRequest` puts that to them
and keeps it — the question, who asked, what it is about, by when — and the answer comes back to whatever was
waiting:

```osy syntax
var request = PutRequest(new NewRequest {
  Kind = RequestKind.Approval, To = manager, Title = "Print 400 of the spring poster?",
  About = Subject.Of("Proof", proof.Id, "Spring poster", version: "v" + proof.Version),
  Within = TimeSpan.FromHours(8), Hours = shopHours,
});
var answer = await Requests.Answer(request);          // in a workflow: parks here, for days if it takes days
if (answer.Approved) { UseApproval(request, "v" + proof.Version); Print(proof); }
```

Nothing in it is about agents. A page, a process and an agent ask the same way.

## Signature      {#signature}
```osy syntax
use Osysharp.HumanRequests@1;                     // needs Osysharp.WorkQueues: a request is an item of its queues

policy AsksPeople       => IsStaff;               // who may put a request (a process asks as the app)
policy OverseesRequests => IsLead;                // who reads every request, calls one off, gives it more time
app.Requests = new RequestsSetup { Queue = "Approvals", RemindAfter = TimeSpan.FromHours(4), MaxReminders = 3 };

HumanRequest PutRequest(NewRequest ask)
class NewRequest {
  RequestKind Kind;                               // Question · Approval · Information
  string Title; string Context; List<RequestFact> Facts;
  Principal? To; string Queue; string OutsideName; string OutsideEmail;       // one person · a queue · somebody outside
  List<RequestOption> Options; List<RequestField> Fields;                      // what it may be answered with
  Subject? About; Subject? WaitingWork;           // what it is about (with its version) · the work that waits for it
  TimeSpan? Within; TimeSpan? ExpireAfter; ServiceHours? Hours;                // late after · gives up after · counted in
  TimeSpan? RemindAfter; TimeSpan? RemindEvery; int? MaxReminders; Principal? EscalateTo;
  static NewRequest For<T>();                     // a request for the members of class T
}

Task<RequestOutcome> Requests.Answer(HumanRequest request)     // wait, in a workflow
RequestOutcome OutcomeOf(HumanRequest request)                 // read, at any time
class RequestOutcome { RequestStatus Status; string Choice; string Comment; bool Answered; bool Approved; bool Rejected;
                       string ByName; Assurance? With; DateTime? At; T As<T>(); }

AnswerResult AnswerRequest(HumanRequest request, RequestAnswer answer)              // as whoever is signed in
AnswerResult AnswerByLink(AnswerLink link, RequestAnswer answer, string name = "")  // through the link
string AnswerLinkFor(HumanRequest request)   void RevokeAnswerLink(HumanRequest request)

void UseApproval(HumanRequest request, string versionNow)      bool ApprovalCovers(HumanRequest request, string versionNow)
void CancelRequest(HumanRequest request, string why)
void ExtendRequest(HumanRequest request, TimeSpan within, string why, TimeSpan? expireAfter = null)
HumanRequest ReplaceRequest(HumanRequest old, NewRequest ask, string why)

// events: HumanRequestPut · Answered · Reminded · Overdue · Expired · Cancelled · Retimed · AnsweredLate
// screens: RequestSheet(id) · ExternalAnswer(link) · WaitingOnPeople(askedByName, waitingType, waitingId, open)
// slot:    RequestSubject(subjectType, subjectId, request, changed) — the kit that owns what a request is about draws it
```

## Description    {#description}

### What can be asked?   {#kinds}
| Kind | The person sees | The answer |
|---|---|---|
| `Question` | the question and a button per `RequestOption` — or a box to write in when it offers none | `Choice` (the option's key), `Comment` |
| `Approval` | the thing, what a yes covers, **Approve** and **Reject** — a no asks why before it sends | `Approved` / `Rejected`, `Comment` (the reason) |
| `Information` | a form: one field per member of the class asked for | `As<T>()` — the class, filled in |

`NewRequest.For<SupplierDetails>()` makes the form from the class: a member's name in words is its label, its `///`
comment the line under it, its type the kind of field (text, a paragraph, a number, a yes or no, a date, the members
of an enum), and `T?` means it may be left empty. The answer is checked against the same fields before it is taken,
and read back as the class.

### Who is asked, and where do they answer?   {#who}
- **One person** (`To`): the request is theirs in the inbox as it arrives ([[workflow-inbox-kit#addressee]]).
- **Whoever works a queue** (no `To`): the first of them to answer has answered it.
- **Somebody with no account** (`OutsideEmail`): nobody inside answers it. `AnswerLinkFor(request)` is the text of a
  [link](https://osysharp.com/reference/security/grants/) that opens that one request; the app mails it, and gives the page a route:

```osy syntax
[On] void MailTheLink(HumanRequestPut e) {
  if (e.Request.OutsideEmail != "") { Mail(e.Request.OutsideEmail, $"{App.BaseUrl}/answer/{AnswerLinkFor(e.Request)}"); }
}
[Route("/answer/{link}")] [AllowAnonymous]
component AnswerPage(AnswerLink link) { render { ExternalAnswer(link); } }
```

An answer is written only by the kit's own function, which asks who is answering before it writes: the person asked,
the person it went on to, whoever holds it, the people of its queue, or the holder of its link — and **never the
person who asked**, even when the request is put to a queue they work themselves. `NewRequest.NotBy` names one more
person who may not answer it though they work its queue: the person whose own request it decides — a refund they
asked for, put to Finance.

### How does the answer come back?   {#answer}
In a workflow, `await Requests.Answer(request)` parks the run until the request closes and answers what came back —
the answer, or that it was called off or ran out of time. Put the request in a step, so a body that is run again
finds the one it already put:

```osy syntax
var request = Workflow.Once("ask-manager", () => PutRequest(new NewRequest { … }));
var answer = await Requests.Answer(request);
```

Code that cannot wait — a page's action, a plain function — hears `HumanRequestAnswered` with `[On]`, or reads
`OutcomeOf(request)` when it looks again. `Requests.Answer` says so if it is called there ([Workflow.InRun (can this code wait?)](https://osysharp.com/reference/workflow/in-run/)).

### What does an approval cover?   {#approval}
One exact version, once. An approval is put with what it is of and the version shown (`About = Subject.Of(type, id,
label, version:)` — a revision number, a hash of what was shown; the asker's own terms). The person is told so on
the sheet, before the buttons. The act it allows calls `UseApproval(request, versionNow)` with the version of the
thing **as it is now**, in the same unit of work:

| | `UseApproval` |
|---|---|
| approved, the same version, not yet used | marks it used — the act goes on |
| the thing changed after the yes | refused: *"… has changed since it was approved, so the approval no longer applies. Ask again, about it as it is now."* |
| already used | refused: an approval is used once |
| a no, called off, ran out of time, not answered yet | refused, with which |

`ReplaceRequest(old, ask, why)` asks again about the thing as it is now and calls the old request off.

### What happens when nobody answers?   {#time}
`Within` is how long it may wait before it is **late**, counted in `Hours` ([ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/)) — the date the
inbox shows for the item is the same instant. `RemindAfter` / `RemindEvery` raise `HumanRequestReminded`, in working
hours, at most `MaxReminders` times; the app sends the reminder its own way.

**Late is not closed.** Past its date the request is overdue: `HumanRequestOverdue` is raised, it goes on to
`EscalateTo` when one is named, and it still waits. Nothing is decided by the silence — an approval nobody gave is
not an approval. It closes unanswered only when it was given a time to give up at (`ExpireAfter`) or is called off
(`CancelRequest`, by whoever asked or whoever oversees requests). `ExtendRequest` moves its date and its reminders,
recorded with who and why.

### What about an answer that comes too late?   {#late}
An answer to a request that already has one, was called off or ran out of time is **kept and not applied**: a
`LateAnswer` row beside the request, with who sent it, how they were known and when; `HumanRequestAnsweredLate` is
raised; the person is told their answer was kept and changes nothing by itself. A link goes on opening its request
after it closed, for exactly this.

### What does a person see?   {#screens}
- **`RequestSheet(id)`** — the request as whoever is looking may read it. In the inbox it is drawn for you under the
  item's own head. For the person asked: who asks, what is waiting on the answer, the context, the facts, what it is
  about as the kit that owns it draws it (`RequestSubject`), when it is wanted by, and the answer a press away. For
  whoever asked: who it waits for; then what came back.
- **`ExternalAnswer(link)`** — the page behind a link: the one request, the form, their name, and what became of the
  answer.
- **`WaitingOnPeople(waitingType:, waitingId:)`** — on the page of the thing that waits: every open request holding
  it up, of whom, since when, and whether it is late. `askedByName:` lists what one process or agent has put.
- **"Approve all"** on the inbox's bulk bar while every picked item is an approval — each approved as the person's
  own answer, so one that is not theirs stays as it was.

### Who may do what?   {#security}
A request is read by the person it is put to, the person it went on to, whoever holds it, whoever asked, the people
of its queue when it names nobody, and whoever `OverseesRequests`. It is made only by `PutRequest`
(`AsksPeople`); answered only through `AnswerRequest` / `AnswerByLink`; an approval's use is marked only by
`UseApproval`; its time is changed only by `ExtendRequest`. A link opens the one request it was made for, for that
call, and nothing else in the app.

## Examples       {#examples}
```osy title="a question, an approval of one version, a form — and a process that waits a month" run app=agent-human-requests
app Printshop {
  use Osysharp.HumanRequests;
  use Osysharp.WorkQueues;
  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.Workflow;

[Role] enum ShopRole { Authenticator, Printer, Manager }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsPrinter => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Printer);
policy IsManager => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Manager);

// The inbox's questions and the kit's two, answered.
policy WorksInbox            => IsPrinter || IsManager;
policy LeadsInbox            => IsManager;
policy AdministersInbox      => IsManager;
policy ManagesOrganizations  => IsManager;
policy CollaboratesOnRecords => IsPrinter || IsManager;
policy AsksPeople            => IsPrinter || IsManager;
policy OverseesRequests      => IsManager;

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

public enum ProofStage { Drafted, WithTheManager, Printed, SentBack }

/// A proof of something to print. Every change to it is a new version.
entity Proof {
  [Required("Name the job."), MaxLength(120)] string Job;
  int Copies = 100;
  int Version = 1;
  ProofStage Stage = ProofStage.Drafted;
  bool Printed = false;
  [MaxLength(400)] string Note = "";
  security { allow read, create, update when IsPrinter || IsManager; }
}

/// What the customer is asked for before a job is delivered.
public class DeliveryDetails {
  /// Where the boxes go.
  public string Address = "";
  /// How many boxes we may leave without a signature.
  public int UnattendedBoxes;
  /// Anything the driver should know.
  public string? Notes;
}

public Subject ProofAsShown(Proof p) { return Subject.Of("Proof", p.Id, p.Job, version: "v" + p.Version.ToString()); }

/// Ask the manager to approve a proof as it stands.
public HumanRequest AskToPrint(Proof p, Person manager) {
  return PutRequest(new NewRequest {
    Kind = RequestKind.Approval, To = manager, Title = "Print " + p.Copies + " of " + p.Job + "?",
    Facts = [new RequestFact("Copies", p.Copies.ToString()), new RequestFact("Version", p.Version.ToString())],
    About = ProofAsShown(p), WaitingWork = Subject.Of("Proof", p.Id, p.Job),
  });
}

/// Print it — covered by the approval of this version, once.
public void Print(Proof p, HumanRequest approval) {
  UseApproval(approval, "v" + p.Version.ToString());
  p.Printed = true;
}

/// A proof's way to the press: the manager is asked, the process waits, and what it does next is what she said.
workflow ProofFlow {
  Tracks = Proof.Stage; Autostart = false; Initial = WithTheManager;
  state WithTheManager {
    enter {
      var request = Workflow.Once("ask", () => AskToPrint(this.Item, Person.Single(x => x.Email == "ida@printshop.example")));
      var answer = await Requests.Answer(request);
      if (answer.Approved) {
        UseApproval(request, "v" + this.Item.Version.ToString());
        this.Item.Printed = true;
        goto Printed;
      }
      this.Item.Note = answer.Comment;
      goto SentBack;
    }
  }
  terminal success Printed { }
  terminal success SentBack { }
}

app.Inbox = new InboxSetup { DefaultQueue = "Shop" };

[TestFixture]
void Shop() {
  var noor = new Person { Email = "noor@printshop.example", DisplayName = "Noor" };
  var ida = new Person { Email = "ida@printshop.example", DisplayName = "Ida" };
  new RoleGrant { Member = noor, Role = ShopRole.Printer };
  new RoleGrant { Member = ida, Role = ShopRole.Manager };
  new OrganizationTier { Name = "Standard", Rank = 1, IsDefault = true };
  var shop = new Queue { Name = "Shop", Position = 1 };
  new QueueMember { Queue = shop, Member = noor };
  new QueueMember { Queue = shop, Member = ida };
  new Proof { Job = "Spring poster", Copies = 400 };
}
principal Noor => Person.Single(p => p.Email == "noor@printshop.example");
principal Ida => Person.Single(p => p.Email == "ida@printshop.example");
Proof Poster() { return Proof.Single(p => p.Job == "Spring poster"); }

[Test(Shop)]
void an_approval_covers_the_version_that_was_approved_and_nothing_it_became() {
  Guid id = Guid.Empty;
  runas (Noor) { id = AskToPrint(Poster(), Ida).Id; }
  runas (Ida) {
    Assert.Equal(["Print 400 of Spring poster?"], Requests.Mine().Select(r => r.Title).ToList());
    AnswerRequest(Requests.Mine().Single(), RequestAnswer.Approve());
  }
  runas (Noor) {
    // The proof is changed after her yes: 400 copies became 4,000.
    var poster = Poster();
    poster.Copies = 4000;
    poster.Version = poster.Version + 1;
    var refused = Assert.Throws<ApprovalNoLongerApplies>(() => Print(Poster(), HumanRequest.Single(r => r.Id == id)));
    Assert.Equal("Spring poster has changed since it was approved, so the approval no longer applies. Ask again, about it as it is now.", refused.Message);
    Assert.False(Poster().Printed);
  }
}

[Test(Shop)]
void whoever_asked_cannot_approve_it_themselves() {
  runas (Noor) {
    var request = AskToPrint(Poster(), Ida);
    var refused = Assert.Throws<RequestRefused>(() => AnswerRequest(request, RequestAnswer.Approve()));
    Assert.Equal("You asked this, so it is not yours to answer. It waits for Ida.", refused.Message);
  }
}

[Test(Shop)]
void a_process_waits_a_month_for_a_person_and_goes_on_with_the_answer() {
  runas (Noor) {
    var poster = Poster();
    Workflow.Run(poster);
    Workflow.Settle(poster);
    Assert.Equal(ProofStage.WithTheManager, poster.Stage);
  }
  TestClock.Advance(TimeSpan.FromDays(30));
  runas (Ida) {
    var mine = Requests.Mine().Single();
    AnswerRequest(mine, RequestAnswer.Reject("The date on it is last year's."));
    Workflow.Settle(mine);
  }
  runas (Noor) {
    Workflow.Settle(Poster());
    Assert.Equal(ProofStage.SentBack, Poster().Stage);
    Assert.Equal("The date on it is last year's.", Poster().Note);
    Assert.False(Poster().Printed);
  }
}

[Test(Shop)]
void a_request_for_the_members_of_a_class_comes_back_as_the_class() {
  Guid id = Guid.Empty;
  runas (Noor) {
    var ask = NewRequest.For<DeliveryDetails>();
    ask.To = Ida;
    ask.Title = "Where does the spring poster go?";
    var request = PutRequest(ask);
    id = request.Id;
    var fields = JsonSerializer.Deserialize<List<RequestField>>(request.FieldsJson);
    Assert.Equal(["Address", "Unattended boxes", "Notes"], fields.Select(f => f.Label).ToList());
    Assert.Equal("Where the boxes go.", fields[0].Help);
  }
  runas (Ida) {
    var missing = Assert.Throws<ValidationException>(() => AnswerRequest(HumanRequest.Single(r => r.Id == id), new RequestAnswer()));
    Assert.Equal("“Address” is needed.", missing.Message);
    AnswerRequest(HumanRequest.Single(r => r.Id == id), RequestAnswer.Of(new DeliveryDetails { Address = "Dock 4, Varvsgatan 12", UnattendedBoxes = 2 }));
  }
  runas (Noor) {
    var details = OutcomeOf(HumanRequest.Single(r => r.Id == id)).As<DeliveryDetails>();
    Assert.Equal("Dock 4, Varvsgatan 12", details.Address);
    Assert.Equal(2, details.UnattendedBoxes);
    Assert.Null(details.Notes);
  }
}
```

## Notes          {#notes}
- **Sending is the app's.** The kit raises `HumanRequestPut` and `HumanRequestReminded`; whether that becomes a mail,
  a text message or a bell is the app's `[On]` handler, with the kit it already uses for it.
- **A request is put in the caller's unit of work.** It exists when that commits — so a request about a record that
  was never saved is never put. Call a request off in a later request, not in the one that put it.
- **Instants are shown as stored (UTC)** on the kit's screens until the platform says a viewer's own zone.
- An [assignment](https://osysharp.com/reference/agent/assignments/) lists the requests attached to it when the app also uses the bridge
  `Osysharp.Assignments.WorkQueues`.

## See also       {#see-also}
- [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; an inbox item in an agent's hands
- [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/) — a request put for an assignment is a piece of its work
- [The WorkQueues kit — queues, who holds what, and the service levels promised on any item](https://osysharp.com/reference/workflow/inbox-kit/) — the queues a request waits in, and the inbox it is answered from
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) · [Workflow.InRun (can this code wait?)](https://osysharp.com/reference/workflow/in-run/) — the wait itself
- [grants (a link that opens one row)](https://osysharp.com/reference/security/grants/) — the link that opens one row
- [Asking for help — an agent that consults a more capable model (Agents kit)](https://osysharp.com/reference/agent/help/) — an agent's own ladder of help, whose last rung is a person
