# Processes — every run of a business process, listed and read from start to now (Processes kit)

> Osysharp.Processes makes the runs of an app's workflows followable by the people who answer for them. The app names which workflows are processes — a class each, with what people call it and how a run's title and history are read — and the kit lists every run by process, state, age and what is stuck, and draws one run from start to now: the process on a canvas with where the run is and the path it took, each state it was in and for how long, what happened there in order, what it spent on models against the limit set for each run, and the runs it started. A process says what it did with Processes.Note; other kits record their own steps on the run.

<!-- id: workflow-processes-kit · area: workflow · stability: preview · html: https://osysharp.com/reference/workflow/processes-kit/ -->

## Summary        {#summary}
```osy syntax
use Osysharp.Processes;

/// Taking a new supplier on.
public class TakingOn : Process<Supplier> {
  public override string Name => "Supplier onboarding";                 // what people call it
  public override string Workflow => "Onboarding";                      // the workflow's own name
  public override WorkflowGraph Graph => Onboarding.Graph;              // what a run is drawn on
  public override string TitleOf(Supplier s) => s.Name;                 // what a run is called in a list
  public override List<WorkflowAuditEntry> HistoryOf(Supplier s) => Onboarding.For(s).Audit;
}
app.Processes = new ProcessesSetup { Processes = [new TakingOn()], RunPage = "/processes/{run}" };

policy FollowsProcesses => IsOperations;                                 // who opens the list and a run

[Route("/processes")]       component RunsPage()        { render { ProcessRuns(); } }
[Route("/processes/{run}")] component RunPage(Guid run) { render { ProcessRunInspector(run); } }
```

## Signature      {#signature}
```osy syntax
class Process<T> : ProcessBase {                       // T: the row the workflow tracks
  virtual string Name; virtual string Workflow; virtual string Purpose;
  virtual WorkflowGraph Graph; virtual TimeSpan? StuckAfter;
  virtual string TitleOf(T item); virtual List<WorkflowAuditEntry> HistoryOf(T item);
}
class ProcessesSetup { List<ProcessBase> Processes; string RunPage;       // app.Processes = …
                       TimeSpan StuckCheckEvery = 30 minutes; }           // how often stuck runs are asked for; zero: never
virtual policy FollowsProcesses => false;                                 // who opens the list and a run
virtual policy ErasesProcessRecords => false;                             // who deletes what the kit keeps of a run

component ProcessRuns(Action<Guid>? opened = null)
component ProcessRunInspector(Guid run)
component ProcessRunPicture(ProcessRunView view, bool tall = false)      // the picture alone
component ProcessRunHistory(ProcessRunView view)                         // "What happened" alone
[Slot] component ProcessMomentPanel(string kind, Guid subject)            // what a line is about, opened in place
[Slot] component ProcessRunPanel(Guid run)                                // more about a run, under its history

Processes.Note(said) · Processes.Waiting(said) · Processes.Done(said) · Processes.Problem(said)   // in a workflow's body
Processes.Here() → Guid?                                                  // record the run now; its row's id
NoteOnProcessRun(ProcessLine line)                                        // from a kit — the app's own code only

[On] void H(ProcessStuck e)                                               // e.Run, e.Workflow, e.State, e.Since, e.Item
TellOfStuckRuns(DateTime now) → int                                       // the kit's clock calls it; the app's own code only

ProcessRunsNow(ProcessFilter filter, int most = 50) → ProcessBoard
ProcessRunOf(Guid id) → ProcessRunView?
ProcessRunAbout(string workflow, Guid item) → ProcessRun?
SetProcessBudget(string workflow, decimal perRun, LlmOnLimit onLimit = Stop) · SetRunBudget(run, limit) · RemoveProcessBudget(workflow)
CostOf(ProcessRun run) → ProcessCost · RunBudgetOf(run) → decimal? · RunSpend(run) → decimal
```

## Description    {#description}

### What is a process?   {#process}
A workflow the app names for the people who follow it. Nothing changes in the workflow: it is still a `workflow`,
run by the same engine. The class says what the kit cannot know — what people call it, and how to read a run's
title and history from the row the run tracks. `TitleOf` and `HistoryOf` are run **as whoever is looking**, so they
answer what that person may read.

### When is a run listed?   {#listed}
From the moment it starts. The platform says when a run starts, moves and ends ([WorkflowRunStarted, WorkflowRunMoved, WorkflowRunEnded — hear a run start, move and end](https://osysharp.com/reference/workflow/run-events/)); the kit
hears it for every workflow the app named a process, so a run is in the list at its first state — also one that
only waits for a person and has said nothing — and the kit keeps where each run stands (`ProcessRun.State`,
`MovedAt`, `Standing`, `EndedAt`) as the engine says it. Nothing is written in the workflow for this.

A run of a workflow the app has **not** named a process is listed from the first thing recorded for it: a line of
its own (`Processes.Note(…)`), or a step another kit records there ([Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)](https://osysharp.com/reference/agent/processes/)).

### Do the pages follow a run while they are open?   {#live}
Yes. `ProcessRuns()` reads the runs again when one starts, moves or ends, and `ProcessRunInspector(run)` reads its
run again when it moves, ends, or a line is said about it — as whoever is looking, each time. Nobody opens the page
again to see where a run has got to.

### What does a process say about itself?   {#notes}
| | |
|---|---|
| `Processes.Note(said)` | something it did |
| `Processes.Waiting(said)` | something it now waits for |
| `Processes.Done(said)` | something it finished |
| `Processes.Problem(said)` | something that went wrong, and that it handled or could not |

Each is kept with the state the run was in when it was said ([`Workflow.Current.State`](https://osysharp.com/reference/workflow/run-facts/)), and is a
unit of work of its own: the line is there whatever becomes of the body that said it. Said outside a workflow's run,
it is refused — there is no run to say it about.

### What does the list show?   {#list}
`ProcessRuns()` draws every run, newest first: what it is about, its process, where it is, how long it has run and
what it has spent. It is narrowed to what is **Running**, **Stuck** or **Ended**, to one process, and by a word of
the title. A run is **stuck** when it has sat in one state longer than its process's `StuckAfter`, or its last step
failed and it has not gone on; the line says which. Each run opens on the page the app named in `RunPage`.

### How is the app told that a run is stuck?   {#stuck}
By an event. Every `StuckCheckEvery` (half an hour unless the app says otherwise) the kit asks which runs still run
and came to their state longer ago than their process's `StuckAfter`, and raises **`ProcessStuck`** for each — once
for each stay: a run that moves and gets stuck again is said again, a run that stays where it is is not repeated,
and a run that has ended is never stuck.

```osy syntax
[On] void TellTheOffice(ProcessStuck e) {
  Notify(OfficeLead(), $"A run of {e.Workflow} has been at {e.State} since {e.Since}.");   // e.Run: the kit's row; e.Item: the row it is about
}
```

The kit's clock is a workflow of its own (`ProcessClock`), made with the first run of a process that says how long
is too long; it costs one indexed read per process each time it asks. `StuckCheckEvery = TimeSpan.Zero` turns it
off: the list still marks stuck runs, and nothing is raised — an app that wants its own cadence calls
`TellOfStuckRuns(now)` from its own schedule ([Schedule (recurring work)](https://osysharp.com/reference/scheduling/schedule/)). What a workflow itself must do when a step
takes too long is a deadline on its state, not this ([Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/), [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/)).

### What does one run's page show?   {#run}
`ProcessRunInspector(run)` reads one run from start to now, and changes nothing:

- **Where it stands** — what it is about, running or ended, and at which state.
- **In figures** — when it started, how long it has run, how long it has been where it is, and what it has spent
  on models against the limit it is held to.
- **The picture** — every state of the process on a canvas ([The flow kit — a node-and-edge canvas whose nodes are your own Osy#](https://osysharp.com/reference/ui/flow-kit/)), the state the run is at marked and
  the path it took drawn. See [how a long process is drawn](#long).
- **What happened** — each state it was in, in order, with when it got there and how long it stayed; and under
  each, what happened there: who took a step and who answered one, a reminder, a deadline that passed, a step that
  failed ([For(entity).Audit](https://osysharp.com/reference/workflow/audit/)), the process's own lines, and the steps other kits recorded. Narrowed to
  *The whole run*, *Steps* or *Problems*. A line that names what it is about has **Open**, which draws
  `ProcessMomentPanel` under it.
- **Runs around it** — the run that started it and the runs it started ([`Workflow.Current.Starter`](https://osysharp.com/reference/workflow/run-facts/)).

### How is a long process drawn?   {#long}
A process of up to eight states is drawn across the page, above its history. One with more is drawn **downwards**,
a rank for each state at its own size, and — where the page is wide enough — stands beside the history instead of
above it; on a phone it is one column. Nothing is declared for this: it follows from how many states the workflow
has.

Two things are left off every picture, because they say the mechanism and not what happened:

- **A way out that every state has.** `on Withdraw { goto Withdrawn; }` written once for the whole workflow is a
  transition from each of its states. Twelve lines to one box bury the way the run went, so they are not drawn —
  unless the run took one — and a sentence under the picture says it once: "From any state: withdraw leads to
  Withdrawn."
- **The word "enter"** on a transition a state's own body makes when it has done its work.

### What does a run cost, and what may it?   {#cost}
`Osysharp.Llm` charges every model call made in a workflow's run to that run ([[agent-llm-spend#runs]]), so what a
run cost is counted as it happens — its own calls, and every call of the agent work it gave.

```osy syntax
SetProcessBudget("Onboarding", 2.00m);          // each run of it: two dollars on models, in all
SetRunBudget(run, 5.00m);                       // this one run: five
```

A call that would pass the limit is refused before it leaves — `LlmBudgetExceeded` for a call the body makes itself;
for an agent's step, work that stops where it stands and answers `Stopped`, which the process handles like any
step that did not finish. Budgets are the app's model budgets: whoever manages model spend sets them, and whoever
may read model usage sees what a run cost.

### Who reads what?   {#who}
`policy FollowsProcesses => …;` says who opens the list and a run — nobody, until the app says. What a run is
**about** is still read through the app's own rules for that row. A reader who follows the process and may not read
the row is shown that the run exists and when it was first seen: not its title, where it stands, its history, or
the lines said about it.

Runs and lines are written by the kit alone — a line from a workflow's run, or by a kit's own handler through
`NoteOnProcessRun`, which nobody signed in may call; where a run stands from what the engine says, and from nothing
else — and a line is never changed. `policy ErasesProcessRecords => …;`
says who deletes what the kit keeps of a run (`ForgetProcessRun`); the workflow's own history is the engine's.

### How does another kit add to a run?   {#kits}
It records a line with what the line is about, and contributes what opening one shows:

```osy syntax
NoteOnProcessRun(new ProcessLine { Run = run, Workflow = workflow, Item = item, State = state,
                                   Said = "The courier collected the parcel.", SubjectKind = "Pickup", Subject = pickup.Id });

[Contribution("Pickup")]
component PickupOpened(string kind, Guid subject) : ProcessMomentPanel {
  render { if (kind == "Pickup") { PickupCard(subject); } }
}
```

`ProcessRunPanel(run)` is a second slot, under the history, for a section about the whole run ([[Slot] and contributions — a place on a page that other kits, and the app, add to](https://osysharp.com/reference/ui/contributions/)).

Two bridges do exactly this, and arrive with the kits they join: [`Osysharp.Agents.Processes`](https://osysharp.com/reference/agent/processes/)
tells each piece of an agent's work given in a run, and [`Osysharp.HumanRequests.Processes`](https://osysharp.com/reference/workflow/process-requests/)
tells each request a run puts to a person — who was asked, and who answered.

## Examples       {#examples}
```osy title="a permit followed from the first thing its process says" run app=workflow-processes-kit
app Permits {
  use Osysharp.Processes;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Workflow;
  use Osysharp.Diagrams;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Workflow;
using Osysharp.Processes;

[Role] enum TownRole { Authenticator, Clerk, Porter }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsClerk => RoleGrant.Any(g => g.Member == user && g.Role == TownRole.Clerk);

// Who follows the town's processes.
policy FollowsProcesses => IsClerk;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}

enum PermitStage { Received, [Label("Site visit")] Visit, Granted, Refused }

entity Permit {
  [Required, MaxLength(120)] string Address;
  PermitStage Stage = PermitStage.Received;
  security { allow read, create, update when IsClerk; }
}

workflow Permitting {
  Tracks = Permit.Stage; Autostart = false; Initial = Received;
  event Inspected(bool sound);

  state Received {
    enter {
      Processes.Done("Checked the drawings against the town plan.");      // a line of the run, said here
      goto Visit;
    }
  }
  state Visit {
    subscribe Inspected(bool sound);
    enter { Processes.Waiting("Waiting for the inspector's visit."); }
    on Inspected(bool sound) {
      when (sound) { goto Granted; }
      default { goto Refused; }
    }
  }
  terminal success Granted { }
  terminal cancel  Refused { Message = "the site did not pass"; }
}

/// A building permit, for the people who follow it.
public class BuildingPermit : Process<Permit> {
  public override string Name => "Building permit";
  public override string Workflow => "Permitting";
  public override WorkflowGraph Graph => Permitting.Graph;
  public override TimeSpan? StuckAfter => TimeSpan.FromDays(14);
  public override string TitleOf(Permit p) => p.Address;
  public override List<WorkflowAuditEntry> HistoryOf(Permit p) => Permitting.For(p).Audit;
}

app.Processes = new ProcessesSetup { Processes = [new BuildingPermit()], RunPage = "/permits/{run}" };

[Route("/")] [Authorize(FollowsProcesses)] [Render(CSR)] [Title("Permits")]
component PermitsPage() { render { Stack(p: 5, maxW: "960px") { ProcessRuns(); } } }

[Route("/permits/{run}")] [Authorize(FollowsProcesses)] [Render(CSR)] [Title("A permit")]
component PermitPage(Guid run) { render { Stack(p: 5, maxW: "960px") { ProcessRunInspector(run); } } }

Permit Apply(string address) {
  var permit = new Permit { Address = address };
  UnitOfWork.Commit();
  Workflow.Run(permit);
  return permit;
}

[TestFixture]
void Town() {
  var clerk = new Person { Email = "clerk@town.example", DisplayName = "Hollis Brandt" };
  var porter = new Person { Email = "porter@town.example", DisplayName = "Sim Okafor" };
  new RoleGrant { Member = clerk, Role = TownRole.Clerk };
  new RoleGrant { Member = porter, Role = TownRole.Porter };
}
principal Clerk  => Person.Single(p => p.Email == "clerk@town.example");
principal Porter => Person.Single(p => p.Email == "porter@town.example");

[Test(Town)]
void a_permit_is_followed_from_the_first_thing_its_process_says() {
  TestClock.Set(new DateTime(2031, 4, 7, 9, 0, 0, DateTimeKind.Utc));
  Guid id = Guid.Empty;
  runas (Clerk) {
    var permit = Apply("14 Mill Lane");
    Workflow.Settle(permit);
    id = permit.Id;
  }
  TestClock.Advance(TimeSpan.FromDays(3));
  runas (Clerk) {
    // The list finds it, and the run tells each state with what was said there.
    var listed = ProcessRunsNow(new ProcessFilter { Show = "running" }).Runs.Single();
    Assert.Equal("14 Mill Lane", listed.Title);
    Assert.Equal("Site visit", listed.StateLabel);
    Assert.Equal("3 days", listed.InState);

    var run = ProcessRunOf(ProcessRunAbout("Permitting", id)!.Id)!;
    Assert.Equal(["Received", "Site visit"], run.Stays.Select(s => s.Label).ToList());
    Assert.Equal("Checked the drawings against the town plan.", run.Stays[0].Moments.Single().Said);
    Assert.Equal("Waiting for the inspector's visit.", run.Stays[1].Moments.Single().Said);
    Assert.Equal(["Received", "Visit"], run.Visited);
    Assert.Equal(4, run.States.Count);

    Permitting.RaiseInspected(Permit.Single(), true);
    Workflow.Settle(Permit.Single());
    Assert.Equal(ProcessStatus.Finished, ProcessRunOf(run.Card.Id)!.Card.Status);
  }
  // Somebody who follows no processes reads none.
  runas (Porter) { Assert.Throws<NotAuthorized>(() => ProcessRunsNow(new ProcessFilter())); }
}
```

```osy title="a run that only waits is listed from its start, and the office is told when it is stuck" run app=workflow-processes-stuck
app Moorings {
  use Osysharp.Processes;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Workflow;
  use Osysharp.Diagrams;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Workflow;
using Osysharp.Processes;

[Role] enum HarbourRole { Authenticator, Master }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsMaster => RoleGrant.Any(g => g.Member == user && g.Role == HarbourRole.Master);
policy FollowsProcesses => IsMaster;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}

enum BerthStage { Asked, Offered, Taken, Lapsed }

entity BerthRequest {
  [Required, MaxLength(80)] string Boat;
  BerthStage Stage;
  security { allow read, create, update when IsMaster; }
}

// Nothing in the workflow says anything of the run: it waits, and is moved.
workflow Berthing {
  Tracks = BerthRequest.Stage; Autostart = true; Initial = Asked;
  event Offer();
  event Accept();
  state Asked   { subscribe Offer();  on Offer  { goto Offered; } }
  state Offered { subscribe Accept(); on Accept { goto Taken; } }
  terminal success Taken  { }
  terminal cancel  Lapsed { }
}

public class BerthAllocation : Process<BerthRequest> {
  public override string Name => "Berth allocation";
  public override string Workflow => "Berthing";
  public override WorkflowGraph Graph => Berthing.Graph;
  public override TimeSpan? StuckAfter => TimeSpan.FromDays(3);            // three days in one place is too long
  public override string TitleOf(BerthRequest r) => r.Boat;
  public override List<WorkflowAuditEntry> HistoryOf(BerthRequest r) => Berthing.For(r).Audit;
}

app.Processes = new ProcessesSetup { Processes = [new BerthAllocation()] };

/// What the harbour office was told.
entity OfficeNotice {
  [MaxLength(300)] string Said = "";
  security { allow read when IsMaster; }
}

[On]
void TellTheOffice(ProcessStuck e) {
  new OfficeNotice { Said = $"{e.Workflow} has been at {e.State} since {e.Since:d MMM}" };
}

[TestFixture]
void Harbour() {
  var master = new Person { Email = "master@quay.example", DisplayName = "Edda Varnes" };
  new RoleGrant { Member = master, Role = HarbourRole.Master };
}
principal Master => Person.Single(p => p.Email == "master@quay.example");

[Test(Harbour)]
void a_run_that_only_waits_is_listed_from_its_start_and_said_when_it_is_stuck() {
  TestClock.Set(new DateTime(2031, 6, 2, 8, 0, 0, DateTimeKind.Utc));
  runas (Master) {
    var asked = new BerthRequest { Boat = "Kittiwake" };
    Workflow.Settle(asked);
  }
  Background.Settle();
  runas (Master) {
    // It has said nothing, and it is in the list, at its first state.
    var listed = ProcessRunsNow(new ProcessFilter()).Runs.Single();
    Assert.Equal("Kittiwake", listed.Title);
    Assert.Equal("Asked", listed.StateLabel);
    Assert.Equal(0, ProcessNote.Count());
    Assert.Equal("Asked", ProcessRun.Single().State);                     // what the kit keeps, as the engine said it
  }

  // Four days on, the kit's clock has asked: the office is told, once.
  TestClock.Advance(TimeSpan.FromDays(4));
  Background.Settle();
  TestClock.Advance(TimeSpan.FromHours(2));
  Background.Settle();
  runas (Master) {
    Assert.Equal("Berthing has been at Asked since 2 Jun", OfficeNotice.Single().Said);
    Assert.True(ProcessRunsNow(new ProcessFilter { Show = "stuck" }).Runs.Single().Stuck);
  }
}
```

## Notes          {#notes}
- A workflow the app has not named a process is still recorded when a kit records a step on one of its runs; it is
  listed under the workflow's own name, without a picture or a history.
- A run of a process started when the engine says it did. For a workflow the app has not named a process, it is
  the first moment anything was recorded for it.
- A run that is stopped where it stands — called off while its workflow has no cancelled state to go to
  ([What happens to work a run stops waiting for](https://osysharp.com/reference/workflow/called-off/)) — reads **Stopped** at the state it was in.
- A run's budget covers the calls made in that run. A run it started is a run of its own, with its own limit; what
  the two cost together is shown on the run that started the other.

## See also       {#see-also}
- [Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)](https://osysharp.com/reference/agent/processes/) — an agent's work as a step of a process, told on the run
- [The inspector — one piece of an agent's work, readable by a person who was not there (Agents.Inspector kit)](https://osysharp.com/reference/agent/inspector/) — one piece of an agent's work, opened from a run's line
- [Workflow.Current (which run is this code in?)](https://osysharp.com/reference/workflow/run-facts/) — how code knows which run it is in
- [Graph — every state and every transition of a workflow](https://osysharp.com/reference/workflow/graph/) · [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — what the picture and the history are read from
