Osy#betaRun it on Osyrin: osyrin.com

Reference / Workflow

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

[On] void <Name>(WorkflowRunStarted e) · [On] void <Name>(WorkflowRunMoved e) · [On] void <Name>(WorkflowRunEnded e)

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.

preview1 example compiled by CIworkfloweventscomposition

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:

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#

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 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#

When is each raised?#

the engine…raises
creates a run — autostarted, Workflow.Run(row), a child a body awaits, a legWorkflowRunStarted, 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 routeWorkflowRunMoved, From → To
enters a terminal stateWorkflowRunMoved into it, then WorkflowRunEnded
stops a run that has no terminal cancel to go to, or cancels it from outsideWorkflowRunEnded, Cancelled
gives a run up — its start failed for good, what it had parked is goneWorkflowRunEnded, 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?#

As any event is heard (Raise(e) — say that something happened): 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).

What may a handler read?#

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 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?#

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?#

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?#

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), Assigned / Finished (milestones)).

Examples#

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#

  • 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#

Related

[On] — hear an event, after the commit that raised it

`[On]` on a function makes it a HANDLER of the event its one parameter is. Every handler of an event — in the app and…

Raise(e) — say that something happened

`Raise(e)` says that the event `e` happened — an instance of a class marked `[Event]`. It is written in the same commit…

Workflow.Current (which run is this code in?)

`Workflow.Current` is the workflow run this code is executing in — one value saying its id, the workflow's declared…

For(entity).Audit

Reads a running instance's lifecycle timeline — every transition, claim, deposit, reminder and refusal as an…

Transitions — where this item may go next

One row per move this instance can make right now, with each arm's guard evaluated against it. A board offers only the…

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…

What happens to work a run stops waiting for

A run that is waiting on child runs — one it awaits, several joined, tasks that wait — and is ended from somewhere else…