LIFECYCLE
Settles the “can it go back to pending?” argument
Every state a thing can reach and every legal move between them. Each arrow says who makes the move, what triggers it, what has to be true first, and what happens as a result. States you can never leave, or never reach, get flagged before anyone reads the diagram.
A long journey and a short cycle
A parcel has twelve states, with failure paths and timeouts, drawn left to right. An invoice has six and is drawn top to bottom. Both are from the Relay showcase.
Click a state to see what is true while in it, who acts next, and the moves in and out.
What it draws
Every field in the spec, and where it shows up.
STATES
- kind
initialgets a start dot.waitingis drawn with a dashed border, for a state parked until something outside happens.terminalis a double-bordered pill with no way out.failureis an error or rejected state, which may be recoverable.initialnormalwaitingterminalfailure- id · label
- Use the enum value from the code as the ID, so the diagram and the column agree on what a state is called.
- actor
- Who acts next while the thing sits in this state, printed in the box as
next: courier. - description
- One sentence on what is true in this state.
TRANSITIONS
- actor · event
- The first line on the arrow:
merchant: cancel. - guard
- The condition, on its own line in brackets:
[no scan has been taken]. Write it without the brackets. - action
- The side effect, after a slash:
/ void the label. - kind
autoandtimeoutare moves the system makes on its own, drawn dashed.failureis an error path, drawn red.normalautotimeoutfailure- label
- Replaces the first line when the event name alone does not read well.
SUBJECT AND LAYOUT
- meta.subject
- What carries the state:
Order.status,Subscription.state. One subject per diagram. - layout.direction
- The happy path runs left to right (
lr) or top to bottom (tb). States are ranked by their shortest distance from the start. Failure and terminal states settle where they are first reached. - row · col
- Override a state's rank when the automatic one is not the story you want to tell.
Where it reads from
No file format describes a state machine on its own, so the skill reads the code that enforces one:
Status enums, and the column that stores them.
switch (status) blocks, state tables, and the service methods that set status =.
Checks such as assertTransition(from, to), and the conditions in front of each assignment.
Scheduled jobs and crons that move records on their own, drawn as timeout.
Check this one before you trust it. Transitions spread across services are the easiest thing to miss. The validator will tell you about a state nobody can reach. It cannot tell you about a transition the agent never found.
The spec you keep
{
"schema_version": 1,
"diagram_type": "lifecycle",
"meta": { "title": "Order", "subject": "Order.status" },
"states": [
{ "id": "draft", "label": "Draft", "kind": "initial",
"actor": "buyer" },
{ "id": "placed", "label": "Placed", "actor": "admin" },
{ "id": "approved", "label": "Approved", "kind": "terminal" },
{ "id": "expired", "label": "Expired", "kind": "terminal" }
],
"transitions": [
{ "from": "draft", "to": "placed", "event": "submit",
"actor": "buyer", "guard": "the basket is not empty" },
{ "from": "placed", "to": "approved", "event": "approve",
"actor": "admin", "action": "issue invoice" },
{ "from": "placed", "to": "expired", "event": "7 days elapsed",
"kind": "timeout" }
]
}Aim for exactly one initial state, and make sure every state can reach a terminal one or is marked as a failure. The validator checks both.
“Can an approved order go back to placed?” The answer is whether the arrow is in the file. The file sits in the repo and gets reviewed like code.
The full contract is schemas/lifecycle.schema.json.
What validate flags
This is where a lifecycle diagram earns its keep. The graph checks are warnings, so a render never breaks, but they point straight at the states a design forgot about.
Errors
Exit 1. Each one names the field.
dangling-refa transition to or from a state that does not existduplicate-idtwo states or transitions sharing an ID
Warnings
Printed, never blocking.
dead-enda state with no way out that is not marked terminalunreachablea state nothing leads to from the startterminal-exita “terminal” state that can be left after allinitialno initial state, or more than oneeventan arrow that does not say what triggers itsizemore than 20 states. Split by subject or phase
The sentences it writes
A docs section can ask for lifecycle.states and lifecycle.transitions. Guards and side effects are quoted as you wrote them, behind a label, never bent into a clause. From the Relay build:
Created moves to Cancelled on cancel. Triggered by: merchant. Only when: no scan has been taken. Side effect: void the label.lifecycle.transitions · parcel.lifecycle
Created is the state a new Parcel starts in. 2 transitions lead out of it. Booked and labelled, still on the merchant's shelf. Acts next: merchant.lifecycle.states · parcel.lifecycle#created
What it connects to
Anchor the guard itself
The diagram says a guard exists. An anchored claim pinned to the guard's code says what it enforces, and it goes stale when someone edits that code.
Docs →The calls behind each move
Most transitions are an endpoint or an event. Keep both in one dashboard, and the move and the route that makes it are one click apart in the sidebar.
Endpoints →One file with the rest
Each lifecycle is its own entry, next to the tables that store the status.
Dashboard →