Summary#
Requires names the conditions standing between an item and progress, and the platform both enforces them and
reports them. The reporting half is the point: a rule the engine will refuse is a rule a screen should be able to
show before anybody presses anything.
Signature#
Requires {
<Name> {
Must = <predicate over this.Item>;
Message = "what to tell a human when it does not hold";
}
…
}Read the live checklist back:
<Wf>.For(item).Requirements // List<RequirementStatus> — the state's AND its slots'
<Wf>.For(item).<Slot>.Requirements // just that one wait'sDescription#
Where you declare it decides what it gates#
This is the distinction to hold, and the two are not variations of one rule:
| declared on | gates | means |
|---|---|---|
a slot (subscribe … { Requires { … } }) | the deposit into that wait | "you may not resolve without a root cause" |
a state (state X { Requires { … } }) | the state's completion | "this state is done when all of these hold" |
A slot's criteria are checked when somebody tries to fill it — an unmet one refuses the deposit and hands back which criteria failed, with their messages. A state's criteria replace the default "every armed slot is satisfied" completion test, which is how a quorum is expressed: three voters armed, done at two.
The checklist returns BOTH, and each row names its slot#
<Wf>.For(item).Requirements returns the state's own criteria first, then each of its slots' in declaration order.
Every row carries Slot — the wait it gates, or null for a state criterion.
That member is not decoration. "Record a root cause before you can resolve" and "triage it before this state is done" are different sentences about different acts, and on one flat list a screen cannot group them or say which button each belongs to.
⚠ The unscoped read used to return the state's criteria ALONE, so a slot-scoped Requires — the commonest kind —
came back empty. The gate still refused correctly, which made it worse than a plain omission: the app rendered a
checklist saying there was nothing left to do and then refused the button. An empty checklist and a satisfied one
are the same screen.
The rows are LIVE#
Every predicate is evaluated against this.Item at the moment of the read, and nothing is stored. Re-reading after a
change gives the new answer — so a form can re-check as fields are filled.
Examples#
A ticket that must be triaged before the state is done, and cannot be resolved without a root cause:
enum Stage { Working, Done }
[Principal] entity Agent {
[Required, MaxLength(80)] string Name;
security { allow read when IsAuthenticated || IsAnonymous; }
}
entity Ticket {
[Required, MaxLength(120)] string Subject;
[MaxLength(200)] string? RootCause;
bool Triaged;
Stage State;
security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}
workflow TicketFlow {
Tracks = Ticket.State;
Autostart = true;
Initial = Working;
event Resolve();
state Working {
// About the STATE: it is not done until this holds.
Requires {
Triaged { Must = this.Item.Triaged; Message = "triage it first"; }
}
// About filling THIS WAIT: the deposit is refused until this holds.
subscribe Resolve() as Resolver {
Requires {
RootCause { Must = this.Item.RootCause != null; Message = "record a root cause"; }
}
}
on Resolver { goto Done; }
}
terminal success Done { }
}
// What is left to do — everything, grouped by what it blocks.
List<Osyrin.Workflow.RequirementStatus> Outstanding(Ticket ticket) {
return TicketFlow.For(ticket).Requirements.Where(r => !r.Met).ToList();
}
// Just the wait's own gate — what to show beside the Resolve button.
List<Osyrin.Workflow.RequirementStatus> BeforeResolving(Ticket ticket) {
return TicketFlow.For(ticket).Resolver.Requirements.ToList();
}Notes#
Requires is not authorization. It gates the base fact, never the person: "is the work complete", not "may you
do this". Who may fill a wait is the slot's Candidates, checked first — a refusal there is
a WorkflowAuthorizationException, while an unmet criterion is a RequirementsNotMet carrying the failed criteria.
A UI tells them apart deliberately: one greys a button with a checklist, the other should not have offered it.
A refused deposit is recorded. The refusal writes a Refused row to the trail, so "why did
this never get resolved" has an answer.
complete when is the other way to say a state is done. complete when (<predicate>) goto <State> states one condition and where it goes; a state-level Requires states several NAMED ones with
messages, and is what you want when a human needs to be told which is missing.
See also#
- subscribe — the slot a
Requiresis declared inside - Transitions — where this item may go next — where the item may go next, the read this checklist sits beside
- Candidates (slot) — who may fill the wait, the gate checked before this one
- Acting on an inbox row (deposit, claim, release) — depositing, which is what a slot's criteria gate