Stateloom Design
ASecurityDesign a hierarchical state machine conversationally through the stateloom / smcraft MCP tools. Use when creating or editing a .smdf.json state machine, calling create_state_machine, add_state, add_event, add_transition, remove_state, list_states, list_events, load_definition or validate_definition, building nested composite states, choosing state kinds (normal, final, history), fixing V001-V005 validation errors, switching the active project document with set_project_file, or reading and lea...
- 4 stars
- 0 votes
- 0 copies
- 0 views
- Added October 9, 2026
Works with
Security analysis
100/100Pro scans all 3 files and shows the line behind each finding
npx -y skills add jgwill/miadi-orchestration-kit --skill stateloom-design --agent claude-codeAre you the author of Stateloom Design?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jgwill-stateloom-design)---
name: stateloom-design
description: Design a hierarchical state machine conversationally through the stateloom / smcraft MCP tools. Use when creating or editing a .smdf.json state machine, calling create_state_machine, add_state, add_event, add_transition, remove_state, list_states, list_events, load_definition or validate_definition, building nested composite states, choosing state kinds (normal, final, history), fixing V001-V005 validation errors, switching the active project document with set_project_file, or reading and leaving notes on the diagram and its states with get_notes and set_notes.
---
# Designing a state machine through the MCP tools
The tools mutate one file: the active `.smdf.json` document. Every call is
`read file → mutate → write file → emit to the live bridge`. There is no in-memory session —
the file *is* the session, so a crash loses nothing and a second surface sees every change.
Read `smdf-reference.md` beside this skill for the full schema, the validation rules, and the
constructs the granular tools cannot express.
---
## Step 0 — Know which document you are editing
Always call this first. Never assume.
```
get_project_file
```
It reports the absolute path, whether a machine already lives there, and bridge status. If it
is the wrong document:
```
set_project_file { "path": "/absolute/path/to/film-preprod.smdf.json" }
```
Both the disk target **and** the live bridge room re-point, because rooms are keyed by
absolute path. A missing file is a legitimate target — the next `create_state_machine` or
`load_definition` writes it. The path must end in `.json`; the convention is `.smdf.json`.
---
## The 17 tools
| Group | Tools |
|---|---|
| Create / mutate | `create_state_machine`, `add_state`, `add_event`, `add_transition`, `remove_state`, `load_definition` |
| Inspect | `get_definition`, `list_states`, `list_events`, `get_project_file` |
| Act | `validate_definition`, `generate_code`, `generate_rispec`, `render_diagram` |
| Document | `set_project_file`, `get_project_file` |
| Notes | `get_notes`, `set_notes` |
The same server also carries ten tools for the **data** beside the machine — entities,
attributes, relationships — in a sibling `.erdf.json` document. That is the `stateloom-erd`
skill. The active document's type is its extension: while an `.erdf.json` is active, the
tools above report "no state machine" and refuse to write one over it;
`set_project_file` back to the `.smdf.json` to continue here.
There is also the MCP resource `smcraft://definition` (the current definition as JSON) and the
MCP prompt `design-state-machine` (a guided design conversation).
---
## The order that works
**Events before transitions. Always.** A transition names an event; if that event does not
exist yet, `validate_definition` reports V003. This is the single most common mistake.
```
1. create_state_machine namespace + name
2. add_event × N every trigger the machine can receive
3. add_state × N parents before children
4. add_transition × N now that both endpoints exist
5. validate_definition read it, fix, re-run until clean
6. render_diagram / generate_code / generate_rispec
```
### 1. Create
```
create_state_machine { "namespace": "Orders", "name": "OrderWorkflow", "asynchronous": false }
```
Writes a skeleton: one event source called `Internal` with no events, and a `Root` state with
no children. `asynchronous: true` makes the Python generator emit `ContextAsync` instead of
`Context`.
Working on an existing machine instead? Point at it and inspect — do not recreate:
```
set_project_file { "path": "/abs/path/existing.smdf.json" }
list_states
list_events
```
`create_state_machine` **overwrites** the file. Use it only for a new machine.
### 2. Events
```
add_event { "id": "OrderCreated", "description": "A customer placed an order" }
add_event { "id": "OrderApproved" }
add_event { "id": "OrderCancelled" }
```
Every event lands in event source index 0 (`Internal`). Event IDs must be unique across all
sources. Descriptions are worth writing — they surface in `list_events`, in the rispec, and in
the generated code's docstrings.
To use several named event sources with feeders, timers or typed parameters, build the JSON
yourself and `load_definition` it (see the escape hatch below).
### 3. States
```
add_state { "name": "Pending", "description": "Awaiting approval" }
add_state { "name": "Active", "description": "Order is being fulfilled" }
add_state { "name": "Completed", "kind": "final" }
```
`parent` defaults to `Root`. `kind` is one of `normal` (default), `final`, `history`.
**Hierarchy** — pass `parent` to nest, and add parents before children:
```
add_state { "name": "Active" }
add_state { "name": "Picking", "parent": "Active" }
add_state { "name": "Packing", "parent": "Active" }
add_state { "name": "Shipping", "parent": "Active" }
```
`Active` is now a composite state: it has children, so it is never entered directly — the
machine enters one of its leaves. A state is composite purely by having children; there is no
separate flag.
`add_state` fails with `Parent state 'X' not found.` if the parent does not exist yet. That
error is your ordering check.
### 4. Transitions
```
add_transition { "state": "Pending", "event": "OrderApproved", "nextState": "Active" }
add_transition { "state": "Picking", "event": "ItemScanned", "nextState": "Packing" }
add_transition { "state": "Active", "event": "OrderCancelled","nextState": "Cancelled" }
```
- `state` — the state that reacts. It must already exist, or the tool errors.
- `event` — must already exist as an event, or V003 fires at validation.
- `nextState` — omit it for an **internal transition**: the event is handled, no state change.
- `condition` — a guard expression, emitted verbatim into the generated code
(`"condition": "order.total > 1000"`).
- `description` — free text.
A transition on a **composite** state (`Active` above) expresses hierarchical inheritance in
the model: every leaf inside `Active` is meant to handle `OrderCancelled`. It draws that way
in every diagram surface, and it is the right way to express a shared exit.
**Codegen caveat, verified:** the Python generator emits event handlers on **leaf** state
classes only. A composite is emitted as a plain `State(kind=StateKind.COMPOSITE)` with no
handlers, so at runtime a leaf will not react to its parent's transition. If the generated
code must honour a shared exit, declare that transition on each leaf as well — or keep the
composite declaration for the model and add the leaf declarations for execution.
### 5. Validate
```
validate_definition
```
Clean output is exactly `✓ Definition is valid. No errors found.` Anything else is a list:
```
Found 2 validation error(s):
[V003] State 'Pending' references unknown event 'OrderApprove'
[V004] State 'Active' targets unknown state 'Complete'
```
---
## Reading validation output
The MCP tool runs a fast structural check with **its own five rule IDs**:
| MCP rule | Means | Fix |
|---|---|---|
| `V001` | No events defined at all | `add_event` |
| `V002` | Duplicate state name | Rename — names must be unique across the whole tree, at every depth |
| `V003` | A transition names an event that does not exist | Add the event, or correct the spelling in the transition |
| `V004` | A transition targets a state that does not exist | Add the state, or correct the `nextState` |
| `V005` | `Root` has no child states | `add_state` |
The **canonical** rule set — V001–V014, run by `smcg` and the Python/TypeScript parsers — is a
different, larger numbering. The MCP check is a subset with overlapping IDs, not the same
list. When you need the full check, run it against the file:
```bash
smcg /absolute/path/to/machine.smdf.json --validate-only
```
The full V001–V014 table is in `smdf-reference.md`. The rules the MCP check does **not** cover
and that bite most often: final states with outgoing transitions (V007), final states with
children (V008), composite states with no children (V012), and duplicate event IDs (V003
canonical).
---
## Common mistakes
| Mistake | Symptom | Remedy |
|---|---|---|
| Adding a transition before its event exists | `[V003] … references unknown event` | Add all events first — that is why step 2 precedes step 4 |
| Adding a child before its parent | `Parent state 'X' not found.` | Parents before children, always |
| A state nothing transitions into | No error; the state is unreachable | `list_states`, then read every `→ [event] → target` line and find names that never appear as a target |
| A state with no way out | No error; the machine deadlocks there | Every non-`final` leaf needs at least one outgoing transition |
| Reusing a name at a different depth | `[V002] Duplicate state name` | State names are globally unique — prefix them (`Active_Picking`) |
| Giving a `final` state a transition | Canonical V007 (`smcg` only, MCP passes it) | Final states are terminal; remove the transition or drop `kind: "final"` |
| Calling `create_state_machine` on an existing machine | The file is silently replaced | `set_project_file` + `list_states` first; use `load_definition` to replace deliberately |
| Editing the file by hand while an agent holds it | Last write wins | Let the hub reconcile — it diffs external edits and broadcasts them (see `stateloom-live-loop`) |
---
## Inspecting as you go
`list_states` prints the tree with transitions, indented by depth:
```
Root
Pending — 1 transition(s)
→ [OrderApproved] → Active
Active — 1 transition(s)
→ [OrderCancelled] → Cancelled
Picking — 1 transition(s)
→ [ItemScanned] → Packing
Packing — 0 transition(s)
Completed (final) — 0 transition(s)
```
`list_events` prints `id — description` per line. `get_definition` returns the whole JSON —
use it before a risky change so you can `load_definition` your way back.
---
## The escape hatch: `load_definition`
The granular tools cover states, events and transitions. The SMDF format carries more:
`parallel` regions, timers, typed event parameters, `onEntry` / `onExit` action lists,
multiple named event sources with feeders, and `settings.objects` / `settings.context`.
To use those, construct the JSON and load it whole:
```
load_definition { "json": "{\"settings\":{…},\"events\":[…],\"state\":{…}}" }
```
It accepts a bare definition or one wrapped in `{ "stateMachine": … }`, replaces the document
entirely, and emits a full-definition update to every live surface. It reports what it loaded:
`Loaded definition → /abs/path.smdf.json: OrderWorkflow (6 states, 4 events)`.
Round-trip safely: `get_definition` → edit the JSON → `load_definition`.
---
## Worked example — a traffic light with a pedestrian phase
```
create_state_machine { "namespace": "City", "name": "TrafficLight" }
add_event { "id": "TimerElapsed", "description": "Phase timer fired" }
add_event { "id": "PedestrianCall","description": "Button pressed at the crossing" }
add_event { "id": "PowerLost" }
add_state { "name": "Operating", "description": "Normal signalling cycle" }
add_state { "name": "Red", "parent": "Operating" }
add_state { "name": "Green", "parent": "Operating" }
add_state { "name": "Amber", "parent": "Operating" }
add_state { "name": "Dark", "kind": "final", "description": "Power failure — signal off" }
add_transition { "state": "Red", "event": "TimerElapsed", "nextState": "Green" }
add_transition { "state": "Green", "event": "TimerElapsed", "nextState": "Amber" }
add_transition { "state": "Amber", "event": "TimerElapsed", "nextState": "Red" }
add_transition { "state": "Green", "event": "PedestrianCall", "nextState": "Amber",
"condition": "green_elapsed > 10" }
add_transition { "state": "Operating", "event": "PowerLost", "nextState": "Dark" }
validate_definition
```
Note what the shape buys: `PowerLost` is declared once on `Operating`, so the model says
"any phase can lose power" in one line; the pedestrian call is guarded so a button press
cannot cut a green short; `Dark` is `final` and therefore has no way out, which is exactly
what a power failure means. This exact machine ships as `example.smdf.json` beside this skill
and passes `smcg --validate-only` unchanged.
Before generating code from it, apply the composite caveat above — add `PowerLost` to `Red`,
`Green` and `Amber` too, or the generated Python will ignore it.
Then look at it and move on:
- `render_diagram { "format": "png" }` — see it (skill: `stateloom-render`)
- `generate_code { "language": "python" }` — run it (skill: `stateloom-codegen`)
- `generate_rispec` — specify it (skill: `stateloom-rispec`)
---
## Notes — read them first, leave your own
A person discussing the machine writes notes into it, on the whole diagram or on one state.
They live in the document (`settings.notes`, `notes` on a state) for whoever opens it next.
```
get_notes # every note, the diagram's own first
set_notes { "target": "Rolling", "notes": "Should a retake reset the slate?" }
set_notes { "notes": "Decided: one machine per shooting day." } # no target = the diagram
set_notes { "target": "Rolling", "notes": "" } # an empty string clears it
```
Call `get_notes` before changing a machine someone else has worked on. `set_notes` replaces
the text. `description` says what a state IS; notes are the conversation about it. They are
not part of the machine: the engines and the code generator ignore them.
---
## When the live canvas is running
Every mutating tool persists to disk and then emits to the hub, so a human watching the web
designer sees each state bloom as you add it. Design in small, narratable steps rather than
one `load_definition` dump — the human is reading the board while you work. That loop is the
subject of the `stateloom-live-loop` skill.
---
## Prompts on states — what an agent resolves there
When the machine describes work an agent does (a discussion it opens, a question it holds, a
watch it keeps), say what the agent resolves in each such state:
```
set_prompt { "state": "Talking", "prompt": "Hold one question: where are we at this degree, and is this a moment to act. End each turn with a question or a proposed conclusion." }
add_state { "name": "Open", "prompt": "Open the discussion in the instrument's topic …" }
```
`description` says what the state is, notes are the conversation about it, the **prompt** is
what to do in it. Engines and code generation ignore it; `get_definition` returns it,
`generate_rispec` prints it under the state ("the agent resolves here"), and the designer
edits it in the state's properties. An empty string clears it.
## Objects
`add_object { "instance": "walk", "class": "Screenwalk" }` declares an object the machine is
constructed with — the name a guard reads (`walk.state`) and its entity. It is what L001/L003 ask
for; in a system, reconcile declares it on its own when an ERD attribute's `stateOf` names the
machine.
A chronicle address for an episode that has no `diagrams/` folder yet is fine: the first write
creates it.
## Describing what is already there
`update_state { "name": "Talking", "description": "…", "kind": "normal", "prompt": "…" }` and
`update_event { "id": "Replies", "description": "…" }` change one state or event in place, and
`remove_transition { "state": "Talking", "event": "Conclude", "nextState": "Concluded_Relabel" }` takes one out —
omitted fields stay. Use them instead of rewriting the machine with `load_definition` after a
scenario built states in auto mode. (Reconcile already gives each event and transition it adds
the words of the message that implied it.)
Files in this skill
- SKILL.md
- example.smdf.json
- smdf-reference.md
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.