Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

The Automations kit — rules that do an app's routine work when something happens

use Osysharp.Automations@0; · policy RunsAutomations => …; · policy ManagesAutomations => …; · [Event] classes are its triggers · [Action] functions its steps · the record's fields its conditions · app.Automations = new AutomationsSetup { Subjects = [new MyRules()] }; · AutomationSubject<TRecord> · On<TEvent>() · AutomationRan · AutomationRules() · /settings/automations

`Osysharp.Automations` runs rules — WHEN something happens to a record (any event about it, or every hour), IF the record is so (its own fields, as the rule's author may read them, and how long it has waited), THEN do the app's own `[Action]`s, in order. A rule runs as the person who saved it, under their own security; a rule never loops; every run raises `AutomationRan`. A record that has events and actions has rules, with nothing more to write; the app adds only the rules it ships and what it computes. The team or a person copies an app's rule to change it — restorable, and lockable.

preview2 examples compiled by CIuikitautomationsactions

Source on GitHub: Osysharp.Automations — github.com/osysharp/automations ↗

Summary#

use Osysharp.Automations@0; gives an app rules people write and keep themselves — and the app writes nothing to say what they are about. What HAPPENS to a record is its events ([Event] — what happens to a record, declared once), what can be DONE to it is the app's [Action] functions ([Action] — what an app offers to be done, by name), and what a rule may ASK of it is its own fields:

[Event("A repair is booked")] public sealed class RepairBooked { public Repair Repair; }   // a trigger
Raise(new RepairBooked { Repair = r });                                                   // where it happens

[Action("Add a label")] public string AddLabel(Repair r, string label) { … }               // a step

The app adds only what its model does not say — the rules it ships with, a condition it computes, the records the hourly clock looks at — in one class about the record type.

Signature#

use Osysharp.Automations@0;                  // with Osysharp.Identity@0, Osysharp.Workflow and Osysharp.Ui

policy RunsAutomations => <who works with rules>;      // REQUIRED — reads the rules, keeps their own
policy ManagesAutomations => <who keeps the team's>;   // REQUIRED — writes the team's, copies and locks the app's

// Optional — only to add to what the model says:
app.Automations = new AutomationsSetup { Subjects = [new MyRules()], MaxChain = 5, KeepFires = TimeSpan.FromDays(90) };

public class MyRules : AutomationSubject<Repair> {
  public override List<AutomationDefault> Defaults();           // the app's own rules — When = On<RepairBooked>()
  public override List<AutomationCondition> Conditions();       // what the app COMPUTES, beside the record's fields
  public override List<Entity> Watching();                      // what the hourly clock looks at
  public override Principal? HolderOf(Entity record);           // whose it is — a person's own rules run on theirs
  public override bool Offers(ActionInfo action);               // leave an [Action] out of rules (default: all)
}

public class MyCondition : AutomationCondition {                // { Key, Label, Kind } — a computed one
  public override List<ActionChoice> Choices();                 // Kind = Choice: what it can be
  public override string Value(Entity record);                  // Choice / Text / Number: the record's value
  public override bool Is(Entity record);                       // Kind = Flag: whether it is so
  public override DateTime? Since(Entity record);               // Kind = Hours: when its clock started
}

On<TEvent>()                                                    // a trigger, as an app's rule stores it
// In a subject, the app's own rules are typed throughout — field, value and step — so a mistake is a compile error:
Test(r => r.Field, Is, value)                                   // a field, and a value of the FIELD's own type
Test(r => r.TimeField, AtLeast, TimeSpan.FromHours(48))         // how long since one of its times
Test(computed, Is, true)                                        // a condition the app computes: bool · decimal · string · TimeSpan
Run(r => SomeAction(r, value, …))                               // a step: the [Action] called on the record
Is · IsNot · Contains · DoesNotContain · AtLeast · AtMost       // how a condition compares (ConditionOp)
[On] void H(AutomationRan e) { … }                              // every run: e.Record, e.Fire, e.Sentence()
AutomationRules(string subject = "")                            // the page's content; served at /settings/automations
string FireSentence(AutomationFire fire)                        // "Rule “…” (as Ines Moreau): Set priority: High."

Description#

When, if, then#

A rule is a TRIGGER — any event about the record (or a type it derives from), by its label, or every hour — CONDITIONS that must all hold, and STEPS run in order.

A condition is one of the record's own fields, or one the app computes, compared with is, is not, contains, does not contain, is at least or is at most. Each field compares by its kind: an enum is a choice of its members, a reference a choice of the rows it may point at (read as the rule's author reads them), words, a number, yes or no, and a time is hours since ("in its status for at least 72 hours") — an hourly rule runs once for each such wait, not every hour of it. A step is one of the app's [Action]s on the record, with the values the rule chose — however many it takes and of whatever kind: a choice, yes or no, a number, a date, a time, a length of time, the details of a structure or the items of a list, each in the input its kind takes. The editor reads them as the run will before it keeps the step ([[function-action#check]]), so a value the action cannot take is said then, not on the record. The first step refused stops the rest.

Rules run AFTER the commit that raised the event — the kit hears every event ([On] — hear an event, after the commit that raised it) — so a rule never acts on something rolled back, and reads the record as it now stands. They run in order — the app's, then the team's, then each person's — and each reads what the one before did. An event no rule is on costs one read.

Only what the rule's author may read#

The fields offered are those the person writing the rule may read: a field their rules mask is neither offered nor accepted when they save, or a rule that fired — or did not — would tell them its value. Mark a field [NotInRules] to keep it out of everybody's rules (Records — what any record is called, where it opens, who may read it).

What if the app's own rule is wrong?#

It does not compile. Every part of a rule the app ships is checked where it is written:

  • the field — a selector, Test(r => r.Bike, Contains, "e-bike"): entity 'Repair' has no property 'Bkie'. Did you mean 'Bike'?
  • the value — of the field's own type, Test(r => r.Stage, Is, RepairStage.Ready): a string there is argument to 'Test': cannot pass 'string' to parameter 'value' of type 'RepairStage'. RepairStage declares: Booked, OnStand, Ready. "How long since" one of the record's times takes a duration — Test(r => r.StageSince, AtLeast, TimeSpan.FromHours(48)).
  • each step — the action CALLED on the record, Run(r => AddLabel(r, "battery-check")), so its arguments are checked like any call's: 'AddLabel' has no parameter 'lable'. Did you mean 'label'?, or cannot pass 'int' to parameter 'label' of type 'string'. A step's values are fixed when the rule is written; one that reads the record, or a call of something that is not an [Action], is a compile error too (Run's parameter is [ActionStep]): 'JobWords' is not an action — mark it [Action] to offer it.

A condition the app computes is named by the object itself (Test(Waiting, AtLeast, TimeSpan.FromHours(72)), where Waiting is one of the subject's own members), and its value's type says which kind it is — refused when the condition reads another. A rule a PERSON builds on the rules page is data instead, and what its record does not offer them is refused when they save it.

Three layers, and a copy is how you change one#

  • The app's rules come from Defaults() and keep the app's updates — a rule is identified by its Key.
  • The team (ManagesAutomations) writes rules for everyone, and may copy one of the app's to change it: the copy replaces it for everyone, and restoring the copy brings the app's back, with whatever the app changed since.
  • A person (RunsAutomations) keeps rules of their own, which run on the records they hold (HolderOf), and may copy one of the app's (or the team's copy) for theirs — replacing it there only.
  • The team may lock one of the app's rules: nobody's copy replaces it. Locked = true in code locks it for good.

Who a rule runs as#

A person's rule, and the team's, run as the person who saved it, under their own security as it is when the rule runs — saving a rule records their consent to each step ([Action] — what an app offers to be done, by name). A person who has since lost a role, or left, cannot do through a rule what they could not do by hand; the run is refused and says so. Nothing that is not [Action] can ever be a step.

The app's own rules run as the app, as its own code does. What an app keeps for a PERSON to do — a workflow event whose [Authorize] asks who raised it — the app's rule cannot do: its run is refused, on the record. A team copy of the rule does it, as the person who saved the copy.

How is a rule kept from looping?#

A step runs as an action, and what it raises belongs to the cascade of the event the rule ran on ([On] — hear an event, after the commit that raised it). A rule runs once in a cascade: when a rule (or one it set off) would set it off again, it is stopped, and the stop is written on the record. A rule set off only by its own step is passed over in silence. A chain longer than MaxChain is stopped too.

Every run on the record#

Each run — what it did, as whom, or why it stopped — is an AutomationFire, and raises the event AutomationRan, which says itself (FireSentence(fire)): a record's timeline (The RecordTimeline kit — what happened to a record, on its own timeline) shows it with nothing to write, and an app with a history of its own writes it there from an [On] handler. It is listed under What ran lately on the page, for those who may read the record, and kept for KeepFires.

What the rules page shows#

AutomationSettingsSections announces Automations to the app's settings area (SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what), and the kit serves it in its SettingsFrame: the rules in the order they run, a copy shown under the rule it replaces, the editor (when, only if, then), and what ran lately.

Examples#

A bike workshop labels its e-bike repairs#

The workshop declares its record, an event, an action — and one rule of its own. What a rule may ask of a repair is its fields.

app Workshop {
  use Osysharp.Identity@0;
  use Osysharp.Automations@0;
  use Osysharp.Workflow;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Automations;

[Role] enum WorkshopRole { Authenticator, Mechanic, Owner }
policy IsMechanic => RoleGrant.Any(g => g.Member == user && (g.Role == WorkshopRole.Mechanic || g.Role == WorkshopRole.Owner));
policy RunsAutomations => IsMechanic;
policy ManagesAutomations => RoleGrant.Any(g => g.Member == user && g.Role == WorkshopRole.Owner);

public enum RepairStage { [Label("Booked")] Booked, [Label("On the stand")] OnStand, [Label("Ready")] Ready }

entity Repair {
  [Required, MaxLength(120)] string Bike;
  RepairStage Stage = RepairStage.Booked;
  DateTime StageSince = DateTime.UtcNow;
  [MaxLength(200)] string Labels = "";
  // Anybody may book one here, so the example runs signed out.
  security { allow read, create, update when IsMechanic || IsAnonymous; }
}

/// A repair was booked.
[Event("A repair is booked")]
public sealed class RepairBooked { public Repair Repair; }

app.Automations = new AutomationsSetup { Subjects = [new RepairRules()] };

/// The workshop's own rule: an e-bike gets its battery checked.
public class RepairRules : AutomationSubject<Repair> {
  public override List<AutomationDefault> Defaults() {
    return [
      new AutomationDefault {
        Key = "e-bikes", Name = "E-bikes are labelled", When = On<RepairBooked>(),
        If = [Test(r => r.Bike, Contains, "e-bike")],
        Then = [Run(r => AddLabel(r, "battery-check"))],
      },
    ];
  }
}

/// Label a repair.
[Action("Add a label")]
public string AddLabel(Repair r, [Label("Label")] string label) {
  if (!(" " + r.Labels + " ").Contains(" " + label + " ")) { r.Labels = (r.Labels + " " + label).Trim(); }
  UnitOfWork.Commit();
  return "Labelled " + label + ".";
}

/// Book a repair — what happens to it is an event, which the rules hear.
public Guid Book(string bike) {
  var r = new Repair { Bike = bike };
  Raise(new RepairBooked { Repair = r });
  UnitOfWork.Commit();
  return r.Id;
}
[Test]
void an_e_bike_is_labelled_when_it_is_booked() {
  var id = Book("Cargo e-bike, front hub");
  Background.Settle();
  Assert.Equal("battery-check", Repair.Single(r => r.Id == id).Labels);
}

See also#

Related

[Event] — what happens to a record, declared once

An event is a class marked `[Event]`: something that happens to a record, declared once by the code that owns the…

[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…

[Action] — what an app offers to be done, by name

Mark a function `[Action]` and the app offers it to be done BY NAME — by a rule, a macro, a tool — with everything a…

Records — what any record is called, where it opens, who may read it

`Records.*` answers facts about a row of ANY type from what the app declared — what it is called (`[Title]`), where it…

The RecordTimeline kit — what happened to a record, on its own timeline

`Osysharp.RecordTimeline` puts what happened to a record on its timeline: every event the app and its kits raise about…

The WorkQueues kit's screens — the rail, the list, an item's head, the team, the settings

`Osysharp.WorkQueues` ships the screens a team works an inbox from, on the Switchboard floor: its views, queues and…

SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what

The kit's settings vocabulary: `SettingsArea` lays out an app's settings — its sections in a grouped nav, the current…

The Notifications kit — telling a person that something needs them

`Osysharp.Notifications` tells a person that something needs them — in the app, a bell with a live count and a list…