Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Local

Why a workflow run is not moving

osy workflow-run <id> [--raise <Event> [--slot <Alias>] [--arg N=V] [--as <login>]] [--json]

Opens one parked workflow run and says why it is sitting there — which state, which waits are outstanding, who is allowed to act on each of them right now, what would happen next, and when a deadline fires. Then steps it: raise the event it is waiting for and watch the transition.

stablelocalcliworkflowdebugging

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 migrate will 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 Requires criterion that is not met.
  • Every clock — the SLAs, the state's Expire, the whole-run Deadline, 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 Candidates rule 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 Finance

The 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#

Related

Seeing what happened when your code ran

Opens a finished run and shows what it did — each step named and pointed at its source line, the values that crossed…

Candidates (slot)

Declares WHO may hold or satisfy a `subscribe` slot. `Candidates` is one expression surface that dispatches on its…

subscribe

Declares that a workflow state waits on an event, and configures the wait — who may hold it, who may hand it on…

Transitions — where this item may go next

One row per move this instance can make right now, with each arm's guard evaluated against it. A board offers only the…

Running a function

Runs one of your app's own functions from the command line — to put the app in a known state, backfill a column, or…