# WorkflowRunStarted, WorkflowRunMoved, WorkflowRunEnded — hear a run start, move and end

> The platform raises an event when a workflow run starts, when it leaves one state for another, and when it ends — completed, failed or cancelled. Any `[On]` handler that names one hears it, in the app or in a kit, with the run's facts: its id, the workflow, the states, the id of the row it tracks, the run that started it, and when. They are ordinary events — written with the change, delivered after it — and an app that hears none pays nothing for them.

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

## Summary        {#summary}
A kit that lists runs, a dashboard that counts them, a notice sent when a process ends — each needs to know that a
run **exists** and where it **stands**, without anything being written in the workflow for its sake. The platform
says so, as three events:

```osy syntax
using Osysharp.Workflow;

[On] void Began(WorkflowRunStarted e) { … }     // a run exists, at its first state
[On] void Moved(WorkflowRunMoved e)   { … }     // it left e.From for e.To
[On] void Ended(WorkflowRunEnded e)   { … }     // it ended: e.How is Completed, Failed or Cancelled
```

## Signature      {#signature}
```osy syntax
public sealed class WorkflowRunStarted {   // a run was started — or adopted by an import
  public Guid Run;  public string Workflow;  public string State;
  public Guid? Item;  public Guid? Starter;  public DateTime At;
}
public sealed class WorkflowRunMoved {     // it left one state for another
  public Guid Run;  public string Workflow;  public string From;  public string To;
  public Guid? Item;  public Guid? Starter;  public DateTime At;
}
public sealed class WorkflowRunEnded {     // it reached a terminal state, was stopped, or could not go on
  public Guid Run;  public string Workflow;  public string State;
  public WorkflowRunEnd How;               // Completed · Failed · Cancelled
  public string Message;                   // the terminal's `Message`, a failure's words, "stopped"; "" otherwise
  public Guid? Item;  public Guid? Starter;  public DateTime At;
}
```

`Run`, `Workflow`, `Item` and `Starter` are what [`Workflow.Current`](https://osysharp.com/reference/workflow/run-facts/) answers inside the run:
its id, the workflow's declared name, the id of the row it tracks, and the run whose body started it.

## Description    {#description}

### When is each raised?   {#when}

| the engine… | raises |
|---|---|
| creates a run — autostarted, `Workflow.Run(row)`, a child a body awaits, a leg | `WorkflowRunStarted`, at its `Initial` state |
| adopts a row at a stated state (`osy import`) | `WorkflowRunStarted`, at that state |
| applies a `goto`, a `Complete When`, a breach's route | `WorkflowRunMoved`, `From` → `To` |
| enters a `terminal` state | `WorkflowRunMoved` into it, then `WorkflowRunEnded` |
| stops a run that has no `terminal cancel` to go to, or cancels it from outside | `WorkflowRunEnded`, `Cancelled` |
| gives a run up — its start failed for good, what it had parked is gone | `WorkflowRunEnded`, `Failed`, with why |

A run that enters the same state again (`goto` to itself) has moved: `From` and `To` are the same name.

### When does a handler hear it?   {#delivery}
As any event is heard ([Raise(e) — say that something happened](https://osysharp.com/reference/function/raise/)): it is written by the commit that makes the change, so a start or a move
that is rolled back raised nothing, and its handlers run after that commit. A workflow step may commit more than once,
and its events are delivered together when the **step** has ended ([[function-raise#in-a-step]]) — so a handler reads
the tracked row as the step left it, never half-way, and one step that goes through three states is heard as three
moves, in order, once it is over.

A handler runs with the app's authority, like every `[On]` handler ([[On] — hear an event, after the commit that raised it](https://osysharp.com/reference/function/on/)).

### What may a handler read?   {#reads}
What the event carries, and whatever its own rules let it read. The events carry **ids and names, never rows**: the
run's own row is the engine's and no app reads it ([For(entity).Audit](https://osysharp.com/reference/workflow/audit/) is how an app reads what happened to a run),
and `Item` is an id — a handler that wants the row reads it, and gets it or not as the row's `security { }` says for
that handler's code.

### Who hears them?   {#who}
Only a handler that **names** one. A handler of every event — `[On] void H(Event e)`, what a timeline, a rule engine
or a webhook sender is — does not hear these three, and they are not in `Events.Catalog()`. They are raised for every
move of every run; a timeline kit in an app with a million transitions would otherwise be handed a million events to
ignore.

### What does it cost?   {#cost}
For an app in which no handler names one: nothing — no row, no statement, on any transition. Which of the three are
heard is a fact of the compiled app, known before a run starts. For an app that hears one, each raise is one event
like any other: its row is written with the transition's own commit, and it is delivered once, however many handlers
hear it ([[function-raise#cost]]).

### How do I notice a run that is stuck?   {#stuck}
Keep when it last moved, and ask. A handler of `WorkflowRunStarted` and `WorkflowRunMoved` writes `e.At` on a row of
its own; "has not moved for two days" is then a query over those rows — run when somebody looks, or on a schedule
(`Osysharp.Scheduling`) that raises an event of your own for each run it finds. The platform raises no "stuck" event:
how long is too long is the app's to say, per workflow and often per state — a deadline the workflow itself declares
is a clock on its state ([Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/), [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/)).

## Examples       {#examples}
```osy title="every run of a workflow is listed from its start, and followed to its end" run app=workflow-run-events
app Bakery {
  use Osysharp.Workflow;
  model "model/**/*.osy";
}

using Osysharp.Workflow;

enum BatchStatus { Proving, Baking, Sold, Binned }

entity Batch {
  [Required, MaxLength(40)] string Loaf;
  BatchStatus Status;
  [MaxLength(40)] string Oven = "";
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

/// What the app keeps of each run: that it exists, where it stands, and how it ended. Nothing in the workflow writes it.
entity Following {
  [Required] Guid Run;
  [MaxLength(40)] string Workflow = "";
  Guid? Item;
  [MaxLength(40)] string At = "";
  [MaxLength(200)] string Path = "";
  [MaxLength(40)] string Ended = "";
  /// What the tracked row said when the move was heard — read by the handler, as it may read it.
  [MaxLength(40)] string OvenSeen = "";
  DateTime? MovedAt;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

/// Every event a handler of EVERY event heard — to show which it does not.
entity Heard {
  [MaxLength(100)] string Type = "";
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

workflow BatchFlow {
  Tracks = Batch.Status; Autostart = true; Initial = Proving;
  event Bake();
  event Bin();
  state Proving {                              // it only waits: nothing here says anything of the run
    subscribe Bake();
    subscribe Bin();
    on Bake { goto Baking; }
    on Bin  { goto Binned; }
  }
  state Baking {
    enter {
      this.Item.Oven = "oven 2";               // written later in the same step than the move into Baking
      goto Sold;
    }
  }
  terminal success Sold   { }
  terminal cancel  Binned { Message = "it did not rise"; }
}

[On]
void Began(WorkflowRunStarted e) {
  new Following { Run = e.Run, Workflow = e.Workflow, Item = e.Item, At = e.State, Path = e.State, MovedAt = e.At };
}

[On]
void Moved(WorkflowRunMoved e) {
  var f = Following.Single(x => x.Run == e.Run);
  f.At = e.To;
  f.Path = f.Path + " > " + e.To;
  f.MovedAt = e.At;
  var batch = Batch.FirstOrDefault(b => b.Id == e.Item);      // the tracked row: an ordinary read, by its id
  f.OvenSeen = batch == null ? "" : batch.Oven;
}

[On]
void Over(WorkflowRunEnded e) {
  var f = Following.Single(x => x.Run == e.Run);
  f.Ended = e.How.ToString() + (e.Message == "" ? "" : ": " + e.Message);
}

[On]
void Everything(Event e) { new Heard { Type = Events.Type(e) }; }

[Test]
void a_run_that_only_waits_is_listed_from_its_start() {
  var batch = new Batch { Loaf = "rye" };
  Workflow.Settle(batch);
  Background.Settle();
  var f = Following.Single();
  Assert.Equal("BatchFlow", f.Workflow);
  Assert.Equal(batch.Id, f.Item);
  Assert.Equal("Proving", f.At);
  Assert.Equal("", f.Ended);                   // it is still going
}

[Test]
void every_move_is_heard_in_order_once_the_step_has_ended_and_then_the_end() {
  var batch = new Batch { Loaf = "rye" };
  Workflow.Settle(batch);
  BatchFlow.RaiseBake(batch);
  Workflow.Settle(batch);
  Background.Settle();
  var f = Following.Single();
  Assert.Equal("Proving > Baking > Sold", f.Path);
  Assert.Equal("Sold", f.At);
  Assert.Equal("Completed", f.Ended);
  // The step went on to write the oven AFTER it moved into Baking; a handler heard the move when the step had
  // ended, so it read the row as the step left it.
  Assert.Equal("oven 2", f.OvenSeen);
}

[Test]
void a_run_that_ends_at_a_cancel_says_so_with_the_terminals_words() {
  var batch = new Batch { Loaf = "spelt" };
  Workflow.Settle(batch);
  BatchFlow.RaiseBin(batch);
  Workflow.Settle(batch);
  Background.Settle();
  Assert.Equal("Cancelled: it did not rise", Following.Single().Ended);
}

[Test]
void a_handler_of_every_event_does_not_hear_them_and_they_are_not_offered_to_rules() {
  var batch = new Batch { Loaf = "rye" };
  Workflow.Settle(batch);
  BatchFlow.RaiseBake(batch);
  Workflow.Settle(batch);
  Background.Settle();
  Assert.Equal(1, Following.Count());          // the handlers that name them heard them…
  Assert.Equal(0, Heard.Count());              // …and the one that hears every event heard none of them
  Assert.False(Events.Catalog().Any(i => i.Type.Contains("WorkflowRun")));
}
```

## Notes          {#notes}
- A handler is in no run: `Workflow.Current` is null there. The event says which run it is about.
- A run started under an older version of the app is named as that version declared it.
- `WorkflowRunEnded` is raised once for a run. A run that was already over when something tried to end it again
  raises nothing.

## See also       {#see-also}
- [[On] — hear an event, after the commit that raised it](https://osysharp.com/reference/function/on/) — hearing an event: order, failures, what a handler runs as
- [Raise(e) — say that something happened](https://osysharp.com/reference/function/raise/) — when a raised event is written and delivered
- [Workflow.Current (which run is this code in?)](https://osysharp.com/reference/workflow/run-facts/) — `Workflow.Current`: the same facts, asked from inside the run
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — what happened to a run, read by the row it tracks
- [Processes — every run of a business process, listed and read from start to now (Processes kit)](https://osysharp.com/reference/workflow/processes-kit/) — every run of a process, listed and read
