Workflows
States, the events that move between them, who may raise each one, and what happens when a step waits for days. A workflow is source you read top to bottom, and a run that outlives a deploy.
01
The state is a column on your entity
A workflow does not carry a state of its own beside your data. It TRACKS a field you already declared — so "what stage is this order at" is one column, one index, and one thing to list on.
workflow OrderLifecycle { 1 Tracks = Order.Status; 2 Autostart = true; Initial = Placed; 3 event Pay(); event Ship(string tracking); event Deliver(); 4 [Authorize(u => RoleGrant.Any(g => g.Grantee == u && g.Level == AppRole.Manager))] event Cancel(); on Cancel { if (this.Item.Status == OrderStatus.Shipped) { var ret = Workflow.Once("make-return", () => new ReturnShipment { Order = this.Item }); 5 await Workflow.Run("ret", ret); // hold until the return workflow refunds goto Returning; } else { goto Cancelled; } } state Placed { subscribe Pay(); on Pay { goto Paid; } } state Paid { 6 subscribe Ship(string tracking) as Fulfil { 7 Candidates = u => RoleGrant.Any(g => g.Grantee == u && g.Level == AppRole.Fulfillment); } 8 on Fulfil(string tracking) { this.Item.Tracking = tracking; goto Shipped; } } state Shipped { subscribe Deliver(); on Deliver { goto Delivered; } } terminal success Delivered { } terminal cancel Cancelled { Message = "order cancelled"; } 9 terminal cancel Returning { Message = "order cancelled after shipping — return + refund issued"; }
The workflow's state IS Order.Status. Not a parallel table joined to it. A screen that lists orders by stage is an ordinary query over an ordinary enum column.
A run begins when an Order row is created. Nothing starts it by hand, so there is no path where a row exists and its run does not.
The vocabulary of what can happen. An event may carry arguments — Ship(string tracking) — and those arguments are what the person doing the work is asked for.
Who may raise it, on the event itself. Not a check inside a handler that somebody can forget: an event a caller may not raise is an event they are never offered.
Awaiting a CHILD workflow parks this one. Durably: no process is held, and the order resumes when the return terminals — which may be minutes or a fortnight later. See the durability guide.
A slot — work offered to a set of people. This is the difference between a state machine and a workflow: the state says the order is paid, the slot says somebody has to ship it, and the slot is a row you can list, assign, chase and measure.
Who it is offered to, as a predicate over your own model. An inbox is then a query, not a feature.
The handler. goto moves the state — and moving it is the only thing that does, so the transition log is complete by construction.
Terminals are typed. success, cancel and error are three different endings, and a reporting screen that wants "how many failed" does not have to infer it from a status name.
02
Cancel from anywhere, and compensate
The on Cancel block sits outside every state, so it is reachable from all of them. What it does depends on where the order actually is — and cancelling something already shipped cannot just flip a flag, because the goods are gone.
on Cancel { if (this.Item.Status == OrderStatus.Shipped) { var ret = Workflow.Once("make-return", () => new ReturnShipment { Order = this.Item }); await Workflow.Run("ret", ret); // hold until the return workflow refunds goto Returning; } else { goto Cancelled; } }
03
What happens when you change it
Workflow changes, and what they cost
CandidatesWhere to go next
What `await` on a child actually costs, and the saga.
An agent's work runs inside one of these.
36 pages, one construct each.