Summary#
subscribe declares that a workflow state is listening for an event, and configures that wait — who may fill it, when
it is armed, what must hold first, and what it promises. It is config only: the guarded routes that fire when the
event arrives (on Event when (…) { … goto … }) live at the state level alongside the wait, not inside the
subscribe block.
Signature#
subscribe <Event>(<typed params>) [as <Alias>] [foreach (<T> <x> in <collection>)] {
Candidates = <principal => bool> | <() => List<Principal>>; // who may HOLD or satisfy it
Reassign = <actor => bool>; // who may hand it ON
Assignee = <expr>; // who it is handed to at arm time
When = <predicate>; // whether it is armed on this run at all
After = [<Slot>, …]; // slots that must be satisfied first
Assigned { … } // the pickup promise — Within, Remind, a breach arm
Finished { … } // the completion promise
Requires { … } // named preconditions on filling it
}
subscribe <Event>(); // bodyless — a wait with no configurationDescription#
A workflow state advances when an event it subscribes to arrives and a matching route transitions it. subscribe
names that event and restates its typed parameters (so the payload is visible at the handler), then configures the
wait itself. Each setting has its own page — see See also — and the shape to hold is that they answer
different questions:
| setting | question |
|---|---|
Candidates | who may hold or satisfy this slot |
Reassign | who besides the holder may move it |
Assignee | who it belongs to from the moment it is armed (a slot with one is not a pool slot) |
When | whether this run gets the slot at all |
After | which sibling slots must be satisfied before it opens |
The promises — how long a slot may sit unclaimed, how long its holder has, the nudges along the way, the retries,
and where a missed one routes — are declared in the Assigned/Finished blocks inside the
slot, not as flat settings on it. That is where Within, Remind, Retries, Backoff and the Unassigned /
Unfinished / Exhausted arms live.
A slot's success routes (on <Event> when (…), on <Alias>(…)) and the state's own timers (on Expire,
on Deadline) live at the state level, not inside the subscribe — every on … in one place.
An entity-typed parameter is a live row, not a copy. A route may read it and write through it, exactly as it can through the workflow's own tracked item, and those writes are saved with the rest of the transition:
event CustomerWithdrawsItem(OrderItem item);
on CustomerWithdrawsItem(OrderItem item) {
item.Status = ItemStatus.Withdrawn; // persisted with the transition
RefundItem(item);
}The event's delivery context (who deposited, the claim) is available to a route that opts into a Deposit
parameter — see on — a route and its arms; it is never ambient.
A state may hold more than one subscribe (multi-wait / N-of-M); that is its own surface — see <span class="planned" title="this page is planned and not written yet">workflow-multiwait</span>.
Examples#
A pool slot the support team may take, a supervisor may move, and which promises to be picked up within an hour:
subscribe Respond() as Reply {
Candidates = u => u.Team == Team.Support && u.OnDuty;
Reassign = a => a.IsSupervisor;
Assigned { Within = TimeSpan.FromHours(1); Unassigned { goto Escalated; } }
}The same wait as an app writes it, in context:
state Draft {
subscribe Submit();
on Submit { goto Review; }
}Minimal — a wait with no config, just the event:
subscribe Submit();⚠ Minimal is not optional. Adding a subscribe to a state adds a wait the state must get through, and on a
state that ends with on Complete — the default "every armed slot is satisfied" test — that means the state now
needs this one too. The line reads like a side channel and behaves like a co-requisite: answer the approval and the
run sits where it is, with no error, no timeout and nothing to inspect, until somebody deposits this event as well.
A slot declaring no Candidates is also open to everybody, so it goes on being offered in
Workflow.Inbox<T>() to every principal for as long as it waits. osy lint reports the shape as
workflow-unowned-slot-gates-completion. If a state should end on one particular wait, route that wait
(on <Slot> { goto <State>; }) instead of leaving it to on Complete; if it ends on a condition over your own
data, say so with complete when or a state-level Requires, both
of which REPLACE the all-slots test.
See also#
- Candidates (slot) — the slot's
Candidatesgate: who may HOLD/satisfy this slot (predicate or computed set), and how a screen ASKS it before offering a button:<Wf>.For(item).<Slot>.Candidates(u) - Assign — handing a slot to a named colleague —
Reassign, and the verbs that hand a slot to a named colleague - Assigned / Finished (milestones) — the
Assigned/Finishedpromises declared inside the slot:Within,Remind,Retries,Backoff, and the breach arms - slot dependencies (After / When / Pending) —
After: the sibling slots that must be satisfied before this one opens - Requires — named preconditions, and the live checklist —
Requires: named preconditions on filling the slot, and the live checklist that reports them - <span class="planned" title="this page is planned and not written yet">workflow-event</span> — the
eventthis subscribes to (a method signature) - on — a route and its arms — the state-level
on <Event> … goto …routes that consume the deposit - <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state and its
Expiredeadline - Tracks and Initial (the field a workflow drives) — how the workflow binds to an entity's enum property