Summary#
A foreach on a subscribe fans one declaration out into MANY parallel wait slots — one runtime slot per element
of the collection. It is how a state waits on several symmetric approvers at once (three reviewers, N referees)
without copy-pasting the slot three times. Over a literal enum list each instance is addressable by the element's
member name, and the loop variable is in scope for the slot's Candidates.
Signature#
subscribe <Event>(<typed params>) as <Alias>
foreach (<ElementType> <var> in [<A>, <B>, <C>]) {
Candidates = <principal> => <predicate using var>;
}Description#
The foreach element type and variable are declared C#-style. At state entry the engine evaluates the collection
and materializes one runtime slot per element, each tagged with that element as its key. The loop variable is a
resolvable value in scope for the slot's settings — Candidates, Assignee, When — so a single predicate
specializes per instance:
- Addressing (literal enum list). The instance alias is the enum member name.
Wf.For(entity).Architecttargets the slot whose element isRole.Architect; its deposit verb is the subscribed event (Wf.For(entity).Architect.Vote(Decision.Approve, "lgtm")). The set of instance aliases is exactly the enum members in the list. - The element variable. Inside
Candidates = u => u.Role == r,ris the element for THAT instance, so the Architect slot only accepts a principal whoseRole == Role.Architect.
Each instance is an ordinary slot: it can be claimed, deposited into, and gated by Requires. The route that fires
on a deposit is keyed on the event (on <Event> { … }) so all instances share one arm; a trailing Slot slot
parameter on that route names WHICH instance answered (slot.Assignee is the depositor).
Casting into an instance does not, by itself, advance the state. Pair a fan-out with a state-level Requires — named preconditions, and the live checklist to express a quorum (“2 of 3 approvals”) — that requirement holding is the state's completion condition, so the remaining instances never have to answer.
A fan-out over a runtime collection (foreach (User u in this.Item.Topic.Referees)) is a separate, dynamic form:
one slot per element resolved at state entry, each pre-assignable to its element, addressed by the acting principal
rather than by a static name. See dynamic fan-out (foreach over a runtime collection).
Examples#
Three symmetric voter slots from one declaration, each gated to its own role, with a 2-of-3 quorum as the completion condition:
enum Decision { Approve, Reject }
enum Role { Architect, Security, Product }
enum ReviewState { Gathering, Accepted, Rejected }
// The entity the workflow TRACKS, and the person it assigns slots to. The example referenced both
// and declared neither — which only went unnoticed while it was exempt from the gate.
[Principal]
entity Person {
[Required, MaxLength(200)] string Email;
Role Role = Role.Architect;
security { allow read, create when IsAuthenticated; }
}
entity Design {
[Required, MaxLength(160)] string Title;
ReviewState Status; // no default: the workflow owns this field (Tracks = Design.Status)
[ForeignKey(Design)] Vote[] Votes;
security { allow read, create, update when IsAuthenticated; }
}
entity Vote {
[Required] Design Design;
Person Voter;
Decision Decision;
[MaxLength(400)] string Comment;
security { allow read, create when IsAuthenticated; }
}
workflow DesignReview {
Tracks = Design.Status;
Autostart = true;
Initial = Gathering;
event Vote(Decision decision, string comment);
state Gathering {
// one declaration → three parallel slots, addressable as .Architect / .Security / .Product
subscribe Vote(Decision decision, string comment) as Voters
foreach (Role r in [Role.Architect, Role.Security, Role.Product]) {
Candidates = u => u.Role == r;
}
on Vote(Decision decision, string comment, Slot slot) {
new Vote { Design = this.Item, Voter = slot.Assignee, Decision = decision, Comment = comment };
}
// the STATE completes when the quorum of approvals holds — the third voter never has to answer
Requires {
Quorum { Must = this.Item.Votes.Count(v => v.Decision == Decision.Approve) >= 2;
Message = "Two of three approvals are required."; }
}
on Complete { goto Accepted; }
}
terminal success Accepted { }
terminal error Rejected { Message = "design rejected"; }
}Driving it from a test (or a UI) — a voter casts on THEIR instance by its role name:
runas (Ada) { DesignReview.For(design).Architect.Vote(Decision.Approve, "lgtm"); }
runas (Sam) { DesignReview.For(design).Security.Vote(Decision.Approve, "ok"); }
// two of three approvals ⇒ Accepted; Product never votedSee also#
- subscribe — the wait this fans out
- Requires — named preconditions, and the live checklist — the state-level quorum that is the completion condition
- <span class="planned" title="this page is planned and not written yet">workflow-route</span> — the event-keyed
on <Event> { … }arm shared by every instance (and itsSlot slotparam) - <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state