Internals
Workflow narrator
State machines to plain English, scenarios and health checks.
Construct reads the real XState machines in a feature's workflows/ folder and explains them in English,
with no LLM and nothing stored. The diagram, the English, the scenarios and the guardrails are all derived
from the same source file every time, so they can never disagree with each other or with the code.
Why it matters: a flow described in a document goes stale the day someone edits the code. Here the description is the code, re-read on every request. A product owner can read the English, a tester can lift the scenarios, a reviewer can see the health findings, and an engineer still edits the one source file.
What you get, per machine#
- In plain English: what each state does, in source order. Identifiers become words (
paymentFailedbecomes "payment failed",isLowValuebecomes "it is low value"), guards become "only if ...", actions become "and then runsnotifyCustomer", delays become "After 48 hours", services become "startsissueRefund". - Scenarios: every distinct route from the start to an end state as Given/When/Then, happy path first (the shortest route to the first end state). Loops are shown once as "this part can repeat"; output is capped at 25 scenarios. Each scenario also has a machine-readable form (event sequence, steps, end state).
- Health: unreachable states, dead ends, no end state, conditional transitions with no fallback, two unguarded transitions on one event, and targets that do not exist.
Use it#
CLI (read-only):
construct research workflow <feature> [<file>] [--format prose|md|json|scenarios] [--dir <path>]
<file> is relative to features/<feature>/workflows/; leave it out to explain every workflow file.
--format json prints pure JSON that can be piped.
UI: the Workflows screen shows the same three panels ("In plain English", "Scenarios", "Health") under each diagram. They refresh on load, on "Re-read from source" and right after a confirmed visual edit.
Guardrails: construct validate reports WORKFLOW-002 (unreachable state) and WORKFLOW-003 (non-final
state with no way out) as warnings for workflow files it can read. Set them to error or off under
rules: in architecture.yml.
WORKFLOW-004 (transition-table completeness) is off by default; opt in with rules: { WORKFLOW-004: warning }
in architecture.yml. An event is any name a machine handles in an on: map of at least one state; every
atomic, non-final state must decide each of them (events a parent state handles count for its children).
Each missing (state, event) pair is one violation at the state's line. To say "this state deliberately does
nothing on that event", write the explicit ignore form, a transition with no target, no actions and no
guard: on: { CANCEL: {} }. Any real transition, or {}, counts as a decision.
Real output#
construct research workflow shop CheckoutWorkflow.tsx, for a workflow generated by
construct create workflow Checkout --feature shop --from fixtures/workflow-graphs/checkout.json:
Checkout
The "checkout" flow has 3 steps. It starts in *idle* and can end in *done*.
Idle
- This flow starts in *idle*.
- When "submit" happens, the flow moves to *submitting*.
Submitting
- When "success" happens, the flow moves to *done*.
- When "failure" happens, the flow moves to *idle* — only if it has error.
Done
- *done* is an end state — the flow stops there.
Scenarios
Happy path
Route: idle → submitting → done
Given the flow starts in *idle*
When "submit" happens
Then the flow moves to *submitting*
And when "success" happens
Then the flow moves to *done*
And the flow ends — *done* is an end state
Note: from *submitting* the flow can go back to *idle* when "failure" happens (only if it has error), so this part can repeat.
Health
- [no-fallback] In *submitting*, "failure" only applies under a condition (it has error) and there is no fallback, so if none holds, nothing happens.
A richer example (guards, a 48 hour timeout, a retry loop, two end states) is
fixtures/workflow-graphs/refund-request.ts; its exact expected output is in
fixtures/workflow-graphs/golden/.
Typed state union (opt-in)#
construct generate workflow Checkout --feature shop --from fixtures/workflow-graphs/checkout.json --state-union
writes the machine as usual and also features/shop/workflows/CheckoutWorkflowState.ts beside it:
CheckoutState, a discriminated union with one member per state and nothing else ({ status: 'idle' } | { status: 'submitting' } | { status: 'done' }).matchCheckoutState(state, { idle, submitting, done }), an exhaustive match: leave a state out andtscfails, naming it. A key the union does not have fails too.assertNeverCheckoutState(state)for thedefaultbranch of aswitch, so a state added to the descriptor later breaks every switch that forgot it.
The file starts with a generated-file marker and is deterministic (two runs give the same bytes). Re-running regenerates it; a file at that path without the marker is real work and is refused, with nothing written. Without the flag, output is unchanged.
Limits#
Only machines written as plain object literals are explained (the same limit as the diagram); others say why they cannot be. Parallel regions and history states are treated as ordinary states. Guards and actions are named, not explained: the English says "it is low value", it does not read the function body.
Where the code lives#
packages/engine/workflowNarrator.mjs (English), workflowScenarios.mjs (scenarios and findings),
workflowExplain.mjs (one call + text renderers), workflowSource.mjs (path-scoped file access for the
CLI); UI in ui/client/features/workflows, endpoint GET /api/workflows/narrative.