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 CancelledSignature#
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 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?#
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.Currentis 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.
WorkflowRunEndedis raised once for a run. A run that was already over when something tried to end it again raises nothing.
See also#
- [On] — hear an event, after the commit that raised it — hearing an event: order, failures, what a handler runs as
- Raise(e) — say that something happened — when a raised event is written and delivered
- Workflow.Current (which run is this code in?) —
Workflow.Current: the same facts, asked from inside the run - For(entity).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) — every run of a process, listed and read