Summary#
Some states end because a particular thing happened. Others end because the world reached a particular shape — every item settled, every reviewer answered, nothing left outstanding. For the second kind, the state is not waiting for an event; it is waiting for a condition.
complete when (<predicate>) goto <State>; says exactly that, once, on the state. After anything happens in that
state the predicate is re-checked, and when it holds the run moves on.
Signature#
state <Name> {
complete when (<predicate over this.Item>) goto <State>;
…
}One per state. The predicate is an ordinary boolean expression with this.Item — the row the workflow tracks — in
scope, so it can call your own functions and read your own tables.
Description#
Without it, the condition has to be repeated at the end of every handler that could be the last thing to happen:
on ShipmentDelivered(Shipment shipment) { …; when (AllItemsSettled(this.Item)) { goto Settled; } }
on ShipmentLost(Shipment shipment) { …; when (AllItemsSettled(this.Item)) { goto Settled; } }
on CustomerWithdrawsItem(OrderItem item){ …; when (AllItemsSettled(this.Item)) { goto Settled; } }That works, and it quietly gets worse as the workflow grows. The obligation lands on every handler you add, and it is invisible at the moment it matters: add a fourth handler, forget the line, and the run stays in that state for ever. Nothing reports it — there is no error, no timeout, no failed step. It simply never finishes.
Declared once, the handlers go back to doing one job each:
state Fulfilling {
// Asked ONCE, on the state — not at the end of each handler. Re-checked after anything happens here.
complete when (AllItemsSettled(this.Item)) goto Settled;
on ShipmentDelivered(Shipment shipment) { shipment.Delivered = true; }
on ShipmentLost(Shipment shipment) { shipment.Lost = true; }
on CustomerWithdrawsItem(OrderItem item) { item.Withdrawn = true; }
}⚠ Written out rather than pulled from an app, because no shipped sample uses complete when yet — the fence is
preview for that reason as well as the surface's own. When a sample adopts it, this becomes a sample= pull like
the rest, and stops being prose that can drift.
An explicit goto still wins. A state can be left for reasons that have nothing to do with being finished — a
cancellation, a customer giving up. A handler that decides to leave goes where it says; the completion condition is
only consulted when the handler did not already transition.
When it is checked. After a handler for that state has run and its changes are committed — so the predicate sees the world that handler left, including the rows it just wrote. It is not a poll: nothing re-checks it while the state is idle, because nothing has changed.
A condition that already holds ends the state at the first opportunity. A job whose work is already complete is complete; that is not a special case to guard against.
Examples#
enum Decision { Approve, Reject }
enum OrderState { Fulfilling, Shipped }
[Principal]
entity Person {
[Required, MaxLength(200)] string Email;
security { allow read, create when IsAuthenticated; }
}
entity Order {
[Required, MaxLength(60)] string Reference;
[Required] Person Requester;
OrderState Status; // no default: the workflow owns this field
security { allow read, create, update when IsAuthenticated; }
}
workflow Fulfilment {
Tracks = Order.Status;
Autostart = true;
Initial = Fulfilling;
event Pack();
state Fulfilling {
subscribe Pack();
on Pack { }
Requires {
Packed { Must = this.Item.Reference != "";
Message = "Every line has to be packed first."; }
}
on Complete { goto Shipped; }
}
terminal success Shipped { }
}An order that is finished when every one of its items has come to rest — whichever way each one got there, and however many shipments it took:
state Fulfilling {
enter { SourcePendingItems(this.Item); }
complete when (AllItemsSettled(this.Item)) goto Settled;
subscribe ShipmentDelivered(Shipment shipment);
subscribe CustomerWithdrawsItem(OrderItem item);
on ShipmentDelivered(Shipment shipment) { … } // records deliveries
on CustomerWithdrawsItem(OrderItem item) { … } // records a refund
}Notes#
This is not a join, and the difference is worth knowing. A join is for "I started this specific set of work and I am waiting for it to come back." A completion condition is for "I am waiting for my own data to reach a shape." The order above cannot use a join: the number of shipments is unknowable when it starts, shipments come and go underneath it, and a lost one puts its items back into the pool to be sent again. What the order waits for is a fact about its items, not a set of tasks.
It is not valid on a terminal. A terminal is where a run ends, so there is no completion left to condition;
declaring one there is a compile error rather than a line that is silently ignored.
See also#
- subscribe — declaring what a state waits for
- fan-out (foreach subscribe) — one slot per element, when you are waiting on a set of others
- Workflow.Run (start a workflow) — starting child work from a state