Summary#
A state can run code at two moments: when a run arrives in it, and when a run leaves it.
state UnderReview {
enter { this.Item.ReviewStartedAt = DateTime.UtcNow; }
exit { this.Item.ReviewMinutes = (DateTime.UtcNow - this.Item.ReviewStartedAt).TotalMinutes; }
}Both are optional, and both run inside the same transaction as the move that triggered them — so if either throws, the move does not happen at all.
Signature#
state <Name> {
enter { <statements> } // on arrival — MAY `goto` to redirect the run
exit { <statements> } // on departure — may NOT `goto`
}Description#
Can enter/exit redirect with goto?#
enter may redirect: enter { if (order.Total > 10000) goto NeedsApproval; } sends the run somewhere else instead of
settling in this state. That works because on arrival, where the run ends up is still the thing being decided.
exit may not, and a goto in one is a compile error. By the time an exit body runs the transition has already
been decided — something chose the destination, and this body is running because of that choice. A redirect here
would either silently override a decision already made, or, if two states' exit bodies each redirected into the
other, bounce between them for ever with no arm to break the cycle.
The refusal names where the decision does belong:
an `exit { }` body may not `goto` — state 'Open' runs its exit body BECAUSE a transition was already decided, so a
`goto` here would override a choice already made (and two states redirecting to each other would never settle). Put
the decision where it is still open: an `on` arm, a `complete when`, or the destination state's own `enter { }`,
which MAY redirect.It reads the same way as the rule for milestones, which likewise may not goto from their enter: a body that runs
as a consequence of a decision does not get to re-make it.
In what order do exit, enter and the deadline run?#
Leaving A for B runs, in one transaction:
A'sexit { }B'senter { }B's waits and deadline are armed
So an exit body sees the world as it was in the state it is leaving, before anything about the destination applies.
Deadlines and Accrues#
A state's SLA clock has already stopped by the time its exit body runs. Accrues measures time spent in the
state, and work done on the way out is not that — so an exit body can never inflate the very measurement it is often
there to record.
Do enter/exit run on a terminal state?#
A terminal state can declare an enter { }, which runs as the run finishes. It can declare an exit { } too, but
nothing will ever run it: a terminal is where runs stop. Prefer putting the work in enter.
Examples#
Recording how long a review took — the pair working together, which is what exit exists for:
entity Review {
[Required, MaxLength(200)] string Title;
DateTime? StartedAt;
int Minutes;
ReviewStatus Status = ReviewStatus.Pending;
}
enum ReviewStatus { Pending, UnderReview, Done }
workflow ReviewFlow {
Tracks = Review.Status;
Autostart = false;
Initial = Pending;
event Begin();
event Finish();
state Pending {
subscribe Begin() as B;
on B { goto UnderReview; }
}
state UnderReview {
enter { this.Item.StartedAt = DateTime.UtcNow; }
// Runs on the way out, whichever arm caused the move — so a second way out of this state cannot forget it.
exit { this.Item.Minutes = this.Item.Minutes + 1; }
subscribe Finish() as F;
on F { goto Done; }
}
terminal success Done { }
}That last point is the practical argument for exit over repeating the line at the end of every arm: a state with
three ways out needs the bookkeeping written once, and a fourth arm added later cannot silently omit it.
See also#
- complete when (a state's own completion condition) — ending a state on a condition rather than on a particular event
- Assigned / Finished (milestones) —
Assigned/Finishedtimers, whose ownentermay notgotoeither - ServiceHours (SLA-accrual windows) — what
Accruesmeasures, and therefore what an exit body is outside of - Tracks and Initial (the field a workflow drives) — the enum whose members these states are