Summary#
Answers the question somebody actually asks about a workflow: this approval has been sitting there for three days —
why? The run is opened by its own id or by the id of the thing it tracks, and the answer names the waits that are
outstanding and, for each one, the people the workflow's own rules would let act. Then --raise moves it.
Signature#
osy workflow-runs # the fleet: which workflows, on which version, parked where
osy workflow-runs --runs # one row per RUN, each with its id
osy workflow-run <id> # why THAT one is not moving
osy workflow-run <id> --raise <Event> [--slot <Alias>] [--arg N=V] [--as <login>]Description#
Two words that are not the same thing
A flow is a durable body suspended mid-execution — an awaited child run, a saga leg, a park point. It has a
stack, and a stack is what a debugger steps. That is what Seeing what happened when your code ran's sibling verbs
osy debug-flows and osy debug-attach address.
A run is a workflow state machine. One waiting on a subscribe slot or on a clock has no stack at all — it is
a row holding a position in a graph, and the only thing that moves it is an event. So a debugger cannot attach to
it, and asking for one is asking for frames that do not exist.
The two meet at exactly one point: a run whose own body parked mid-execution does own a continuation, and when it
does, this command prints the osy debug-attach line for it.
What the read tells you
Give it a run and it answers in one call:
- Where it is — the state, how long it has been there, and the version its definition resolves through. If the
state is one your current source no longer declares, it says so: that run is executing a definition you can no
longer read, and
migratewill not report it because it is already on the current version. - What it tracks — the row, by its readable name, and the value of the property the workflow tracks.
- Every outstanding wait — the slot's name, the event it accepts and that event's parameters, whether it is unassigned or held, and by whom.
- Who may act — and this is the field the whole read exists for. See below.
- What happens next — one row per move, each naming the wait it fills, with its guard evaluated against this run right now. A move whose guard depends on the event's own payload stays offered and is marked as needing input: raising it is exactly what supplies the missing value.
- What is blocking it — any
Requirescriterion that is not met. - Every clock — the SLAs, the state's
Expire, the whole-runDeadline, and the reminders, each with when it fires. A deadline already passed reads as overdue, not as a negative interval.
"Nobody has picked it up" and "nobody CAN" are different bugs
Every other screen shows an unclaimed wait the same way whoever is looking at it. But an unassigned slot has two completely different causes:
- a person has not got round to it — someone will;
- the slot's
Candidatesrule admits nobody — a lead went on leave, a department was renamed, the only reviewer who matched is also the requester. Nobody can ever claim it, and the run will sit until a deadline fires.
So the candidate set is evaluated now, through the same rule every claim and deposit is checked against, and an empty one is stated in words rather than printed as a zero. Where the pool is large the list is a sample and says so.
Stepping it
--raise <Event> raises the event the run is waiting for and prints where it landed.
It runs as a real principal: --as <login> with that person's own password, exactly like
Running a function. The workflow's own rules then apply unchanged — an event declaring [Authorize] and a
slot declaring Candidates refuse a caller who does not satisfy them, and a raise with no principal named is
anonymous, which such a workflow correctly refuses. Holding developer authority over an app admits the call; it
never decides what the app's own rules allow.
--slot <Alias> names the wait being filled. It is needed whenever two slots subscribe one event — the ordinary
four-eyes shape, where Approve sits on both a legal and a finance wait — because the event name alone does not say
which one you are answering. The read prints the exact command to copy, with the slot already in it.
Getting an id
osy workflow-runs groups runs, which is what makes it readable as a fleet view and is also why it carries no ids.
--runs lists them individually. You rarely need it: the id of the tracked row works too, and that is the one
already in front of you — in a URL, in a page, in the row you were just looking at. Either id may be given by its
first few characters, which is what the listing prints.
Examples#
An approval that has not moved. The run is opened by the purchase order's own id:
$ osy workflow-run a6428528
PoApproval · Review (waiting) run d77d3242-3b95-4cf4-8b1f-88d1eb42eb24
PurchaseOrder "Rack of servers" · Status = Review
in this state 21m · started 22m ago · version 1.0.0
WAITING ON
╭─────────┬─────────┬────────────┬───────────────────────────┬───────────╮
│ slot │ event │ status │ who can act │ breaches │
├─────────┼─────────┼────────────┼───────────────────────────┼───────────┤
│ Finance │ Approve │ assigned │ [email protected] (assigned) │ in 3h 38m │
│ Legal │ Approve │ unassigned │ 3 candidates │ in 3h 38m │
╰─────────┴─────────┴────────────┴───────────────────────────┴───────────╯
Legal candidates: [email protected], [email protected], [email protected]
WHAT HAPPENS NEXT
Approve → Rejected (fills Legal)
Approve → Rejected (fills Finance)
Cancel → Cancelled
CLOCKS
reminder on Legal — fires in 1h 38m
Assigned SLA on Legal — fires in 3h 38m
run Deadline (whole run) — fires in 29d 23h
Step it: osy workflow-run d77d3242-… --raise Approve --slot Legal --arg decision=<Decision> --arg reason=<string> --as <login>Nobody is holding the legal wait, and three people could. That is a person, not a rule. Had the pool been empty it would say so instead, in exactly the place the three names are.
Stepping it as one of them:
$ osy workflow-run d77d3242 --raise Approve --slot Legal \
--arg decision=Approve --arg reason="within budget" --as [email protected]
✓ Raised Approve on PoApproval as [email protected].
now in Review, waiting on FinanceThe legal wait is filled and the finance one is not, so the run stays in Review — which is the workflow behaving
correctly, and the thing you were checking.
Raising a gated event with nobody named is refused, and the refusal says which rule refused it:
$ osy workflow-run 27b71e0c --raise Cancel
✗ event 'Cancel' requires an authorized principal to raise it.
This raise was ANONYMOUS. An event with `[Authorize]`, or a slot with `Candidates`, refuses that by design —
name a principal with `--as <login>`.Finding a run when you have no id at all:
$ osy workflow-runs --runs
╭──────────┬────────────┬────────┬─────────────────┬────────────────┬──────────╮
│ run │ workflow │ state │ item │ waiting on │ in state │
├──────────┼────────────┼────────┼─────────────────┼────────────────┼──────────┤
│ 27b71e0c │ PoApproval │ Draft │ Standing desks │ Submit │ 16m │
│ 33fd08f6 │ PoApproval │ Draft │ Legal retainer │ Submit │ 16m │
│ d77d3242 │ PoApproval │ Review │ Rack of servers │ Finance, Legal │ 15m │
╰──────────┴────────────┴────────┴─────────────────┴────────────────┴──────────╯See also#
- Candidates (slot) — the rule that decides who may act on a wait
- subscribe — declaring the wait itself
- Transitions — where this item may go next — the same "what happens next" read, from inside the app
- Seeing what happened when your code ran — what a run DID, when the question is a fault rather than a wait
- Running a function — the other verb that acts as one of the app's own users