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 stepThe 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'.RepairStagedeclares: 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 itsKey. - 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 = truein 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#
- [Event] — what happens to a record, declared once — what a trigger is, and how the app raises one
- [On] — hear an event, after the commit that raised it — how the kit hears every event, after the commit
- [Action] — what an app offers to be done, by name — what a step is, and how a rule runs as its saver
- Records — what any record is called, where it opens, who may read it — the fields a rule may ask, and
[NotInRules] - The RecordTimeline kit — what happened to a record, on its own timeline — a record's timeline, where every run shows
- The WorkQueues kit's screens — the rail, the list, an item's head, the team, the settings — the queue a desk's records usually sit in
- The Notifications kit — telling a person that something needs them — telling people, from a step
- SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what — the settings area the page is announced to
kits/automations/tests/— every behaviour above as a test, the screens under--pixels