Summary#
A workflow is a run that outlives the request that started it. It parks — on a person, on a timer, on a child run — and resumes later, possibly days later, possibly in a deployment that did not exist when it began. Everything below follows from that one fact.
enum InvoiceStatus { Submitted, Approved, Escalated }
entity Invoice {
[Required, MaxLength(60)] string Reference;
decimal Amount;
InvoiceStatus Status = InvoiceStatus.Submitted;
}
workflow Approval {
Tracks = Invoice.Status;
Initial = Submitted;
event Approve();
state Submitted {
Expire = TimeSpan.FromDays(2); // nobody acted → the clock acts
subscribe Approve(); // a person acts → a transition
on Approve { goto Approved; }
on Expire { goto Escalated; }
}
terminal success Approved { }
terminal cancel Escalated { }
}Description#
Three properties separate a workflow from a long function, and each is the source of a different first-day mistake.
It parks, so a step needs a name. Awaiting a child run suspends this one. While it is suspended the app can be recompiled and redeployed, and the resuming version has to recognise which step the run is sitting at — so every awaited step carries a label you write, a literal string, unique within the workflow. See Step labels (naming a child run so it survives a new version). A fire-and-forget start never parks and needs none.
It replays, so leaving the platform is special. On resume the engine re-executes the body up to where the run had reached. A call that already went out must not go out twice — a charge, an email, a webhook. You do not mark those: the compiler makes every outbound call a durable step and replays its recorded answer instead of repeating it (Automatic durability (steps you do not have to write)). What you must not do is reach for a clock or a random number as if they were ordinary — see Wall-time clocks (Accrues = false).
It is a state machine, so what may happen next is data. Workflow.Transitions(item) answers the moves this
instance can make right now, each arm's guard already evaluated (Transitions — where this item may go next) — a board offers the
lanes a card can actually reach instead of accepting a drop and having the engine refuse it afterwards.
Workflow.Raise(item, move) takes one of them by handing the row back.
Who holds the work is also data. Workflow.Inbox<T> (what is waiting for me) is what this principal may act on; Workflow.Work<T> (everything outstanding) and its SLA numbers is every live slot of every run, whoever holds it, with the deadline budget attached. They are the same rows read from two viewpoints, and picking the wrong one is how an operator screen quietly becomes a personal one.
The pages#
Run osy docs workflow for the full listing. The groups:
- Declaring one — Workflow.Run (start a workflow), Tracks and Initial (the field a workflow drives), enter and exit (a state's arrival and departure hooks), Transitions — where this item may go next, Requires — named preconditions, and the live checklist, [Authorize] (event), What a workflow body may write
- Waiting — subscribe, Remind (milestone reminders), Workflow.Once (run a step at most once), ServiceHours (SLA-accrual windows), Wall-time clocks (Accrues = false), Backoff (retry policy)
- Branching and joining — fan-out (foreach subscribe), dynamic fan-out (foreach over a runtime collection), Parallel legs (start several, then wait for them), complete when (a state's own completion condition), slot dependencies (After / When / Pending)
- Work and people — Workflow.Inbox<T> (what is waiting for me), Acting on an inbox row (deposit, claim, release), Workflow.Work<T> (everything outstanding) and its SLA numbers, Workflow.WorkByItem<T> (one row per item — the board read), Assign — handing a slot to a named colleague, Candidates (slot), Assigned / Finished (milestones)
- Surviving deployment — Step labels (naming a child run so it survives a new version), Migrating runs that are still in flight, Workflow.Retarget (re-base the SLA clocks), Workflows that outlive the code that started them, Automatic durability (steps you do not have to write)
- Watching it — For(entity).Audit, Flow metrics — how long an item took, and how much was waiting, When a child is cancelled or fails
See also#
- Workflow.Run (start a workflow) — starting one, and the difference between awaiting and firing and forgetting
- Automatic durability (steps you do not have to write) — why you never mark an outbound call
- Step labels (naming a child run so it survives a new version) — the one thing you must write by hand for a run to survive a deploy