CLI Tool
Generate, inspect, simulate, diagram and document XState machines from the terminal with the xsm command.
xsm is the command-line companion to the library. It reads XState-compatible JSON machine definitions — exported from Stately.ai or written by hand — and does the following with them (the All Commands Overview lists every subcommand):
| You want to… | Command |
|---|---|
| Turn a machine into runnable, typed Python (plus tests, typed context, a plugin skeleton) | xsm generate-template (gt) |
| See its state tree, transitions, logic and policies at a glance | xsm inspect (ins) |
| Run it live — pick events, advance the clock, undo — or replay a script in CI | xsm simulate (sim) |
| Export a Mermaid, PlantUML or ASCII diagram | xsm diagram (dia) |
| Produce a Markdown reference page per machine | xsm docs |
| Prove a file builds with the real library and list every finding | xsm validate (val) |
Run xsm with no arguments on a terminal and you get an interactive launcher: a menu, recent files, and a generate wizard with a live preview. Everything the launcher can do, a flag can do too — it is a thin layer of prompts over the same functions.
Like the library, the CLI has zero runtime dependencies. Colour, box-drawing, spinners and single-key input are implemented on the standard library; when output is piped (or --plain is given) every command degrades to deterministic plain text, so xsm … | grep and CI logs stay clean.
🎨 Presentation flags
These work before or after the subcommand:
| Flag | Effect |
|---|---|
--plain |
Plain text: no colour, no box glyphs, no animation. Implied whenever stdout is not a terminal |
--no-color |
Keep layout and animation but emit no colour escapes. Also honours the NO_COLOR convention and TERM=dumb |
--no-anim |
Disable spinners and in-place redraws (XSM_NO_ANIM=1 does the same) |
--verbose |
Show the library’s INFO log on stderr while a command runs (default: warnings only) |
Truecolor is used when COLORTERM=truecolor/24bit, otherwise the 256/16-colour tiers; Windows consoles get virtual-terminal processing enabled automatically. Unicode glyphs are only emitted when the stream can encode them; on an ASCII console the separators and arrows are transliterated (->, -, ...) rather than replaced with ?.
🚀 Basic Usage
# Generate 2 files: logic + runner (default)
xsm generate-template my_machine.json
# Short alias — identical behavior
xsm gt my_machine.json
# …plus a pytest module and a typed-context module
xsm gt my_machine.json -t pythonic-class --with-tests --with-types
This produces two files alongside my_machine.json:
my_machine_logic.py # Action, guard, and service stubs
my_machine_runner.py # Interpreter bootstrap + event simulation
Tip: If the
xsmcommand is not found after installing the package, usepython -m xstate_statemachineinstead. On a Windows machine wherexsm.exeis blocked by an Application Control policy, see the Windows note below.
📂 What Gets Generated
Logic File (*_logic.py)
The logic file contains stub implementations for every action, guard, and service referenced in your JSON config. Depending on the template you choose, these are either:
- Methods on a class (templates:
pythonic-class,class-json) - Module-level functions (templates:
pythonic-builder,pythonic-functional,function-json)
Each stub includes:
- Correct function signature with type hints
- Rich docstring explaining the component
- Logging statement (if
--log yes) - Error handling with try/except (for actions and services)
- A
# TODO: implementmarker for you to fill in
Runner File (*_runner.py)
The runner file contains everything needed to instantiate and run your state machine:
- JSON config loading (or Pythonic API construction, depending on template)
- Logic binding (class instance or module reference)
- Interpreter creation and startup
- Event simulation loop with all events found in your config
- Graceful shutdown
You can run it immediately:
python my_machine_runner.py
⚙️ All CLI Options
xsm generate-template [JSON_FILES...] [OPTIONS]
Complete Options Reference
| Flag | Long Form | Type | Default | Description |
|---|---|---|---|---|
| (positional) | json_files |
FILE... |
— | One or more JSON config files to process |
-j |
--json |
FILE |
— | Additional JSON input file (repeatable) |
-jp |
--json-parent |
FILE |
— | Designate the parent machine for hierarchy |
-jc |
--json-child |
FILE |
— | Designate child machine(s) for hierarchy (repeatable) |
| — | --from-django-fsm |
app.Model |
— | Extract the chart from a django-fsm-2 model (needs DJANGO_SETTINGS_MODULE; no database access); see vs django-fsm |
| — | --fsm-field |
FIELD |
state |
The FSMField for --from-django-fsm |
-t |
--template |
CHOICE |
class-json |
Code generation template (see below) |
| — | --with-tests |
flag | false |
Also emit test_<machine>.py — a pytest module recorded from the real engine |
| — | --with-types |
flag | false |
Also emit <machine>_types.py — TypedDict context, Literal events, typed stubs |
| — | --with-plugin |
flag | false |
Also emit <machine>_observer.py — a PluginBase wired for this chart’s hooks |
| — | --with-api |
flag | false |
Also emit <machine>_api.py — an editable FastAPI router, one typed route per event ([fastapi] extra) |
| — | --with-models |
flag | false |
Also emit <machine>_models.py — a pydantic context model and one EventModel per event ([pydantic] extra) |
| — | --fixtures |
flag | false |
pytest template only: emit @pytest.mark.xstate_machine and the xsm_interp / xsm_clock / xsm_ran fixtures of the [testing] extra instead of building the interpreter by hand (see Integration testing) |
-s |
--style |
CHOICE |
— | DEPRECATED — use --template instead |
-o |
--output |
DIR |
(same as JSON) | Output directory for generated files |
-fc |
--file-count |
{1, 2} |
2 |
Number of output files: 1 = merged, 2 = separate |
-f |
--force |
flag | false |
Overwrite existing files without prompting |
-am |
--async-mode |
yes/no |
(template-dependent) | Generate async or sync code |
-l |
--loader |
yes/no |
yes |
Use LogicLoader auto-discovery in runner |
| — | --log |
yes/no |
yes |
Include logging statements in generated code |
| — | --sleep |
yes/no |
yes |
Add sleep calls between events in simulation |
| — | --sleep-time |
INT |
2 |
Sleep duration in seconds between events |
| — | --check |
flag | false |
Write nothing (not even the -o dir); exit 1 if any file — primary or companion — is out of date or missing, 2 on usage errors |
| — | --diff |
flag | false |
Like --check, plus a unified diff. Implies --check |
| — | --no-verify |
flag | false |
Skip the structural fidelity check (syntax is still checked) |
-v |
--version |
flag | — | Show version number and exit |
Option Details
Template (-t / --template)
Five primary templates produce a logic module and a runner:
| Template | Description |
|---|---|
pythonic-class |
StateMachine subclass with @action / @guard / @service decorators |
pythonic-builder |
MachineBuilder fluent chain with decorated module-level functions |
pythonic-functional |
State objects + build_machine() call with decorated functions |
class-json |
Class with camelCase methods, JSON loaded at runtime (default) |
function-json |
Module-level functions, JSON loaded at runtime |
Five companion templates produce a single extra module and can be requested either on their own (--template pytest) or alongside any primary template with --with-tests / --with-types / --with-plugin / --with-models / --with-api:
| Template | File | What it contains |
|---|---|---|
pytest |
test_<machine>.py |
A test module recorded from the real engine: the initial state, one test per reachable event step (state before, event, state after, actions run) on a SimulatedClock, and a guard-denial test where a guard exists. Uses stub logic so it runs green immediately and stays green until the JSON changes |
typed |
<machine>_types.py |
Context as a TypedDict (from the JSON context), Event = Literal[...] over every declared event, StateId = Literal[...], and a typed MachineLogic[Context] stub with one correctly-annotated function per action, guard and service |
plugin |
<machine>_observer.py |
A PluginBase subclass that overrides exactly the hooks this chart can fire — on_action_error only if it has actions, on_service_error only if it invokes something, on_chain_budget_exceeded only if it can raise/self-send, and so on — each with a docstring naming the states involved |
pydantic-models |
<machine>_models.py |
A context BaseModel inferred from the JSON context, one EventModel per declared event (fields from a declared payload, else extra="allow"), EVENT_MODELS and event_schemas(). Imports only the [pydantic] extra |
fastapi-router |
<machine>_api.py |
An editable FastAPI APIRouter: GET /{id}, one POST /{id}/events/<EVENT> per declared event (typed by the EventModels when --with-models is also on), Depends(get_interpreter), ReceiptResponse, and an authorize stub that raises until you implement it. Imports only the [fastapi] extra |
Companions are compiled and import-checked before writing, participate in --check / --diff, and are listed by xsm list-templates under “Companion outputs”. When the extra is installed, the fastapi-router output is also mounted on a throwaway FastAPI() and its openapi() must list one route per event; without the extra the CLI prints a note and checks syntax only.
Checking generated code in CI (--check / --diff)
--check regenerates in memory and compares the text with what is on disk — the primary logic/runner files and every requested companion. It writes nothing, not even the -o directory, and reports every stale file before failing.
| Exit | Meaning |
|---|---|
0 |
Every generated file is up to date |
1 |
At least one file is out of date or missing (“would be created”), or generation was refused |
2 |
Usage error (bad flags, -o names an existing file) |
--diff is --check plus a unified diff per stale file on stdout (--- <file> (on disk) / +++ <file> (generated)), so a CI log shows exactly what changed. -f has no effect with --check. --check compares text only: it does not import or mount the web companions, so it needs no extra beyond generation and --no-verify makes no difference to it.
Pass the same companion flags you generated with: a companion on disk that this run did not request (say order_api.py after you dropped --with-api) is named in a warning but neither checked nor deleted. When a write would replace a file whose banner names a different source chart (two charts with the same id into one -o), the CLI warns — give the charts distinct ids or separate -o directories.
Hierarchical runs (-jp parent.json -jc child.json) name every file after the parent machine, companions included.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Refusing to generate … |
The chart cannot be represented (no states, a target at the root, …). Fix the chart; the message names the state |
out of date from --check in CI |
Someone edited the JSON without regenerating. Run the same command without --check and commit the result |
ModuleNotFoundError: fastapi / pydantic when importing <machine>_api.py / _models.py |
Install the extra: pip install "xstate-statemachine[fastapi]" (router) / [pydantic] (models) |
… was generated from a.json; overwriting it with code for b.json |
Two charts share an id. Use distinct ids or one -o per chart |
<machine>_api.py (fastapi-router) exists but was not requested |
Leftover from an earlier --with-api run. Add the flag back or delete the file |
| Note: verification skipped (extra not installed) | Generation only needs the CLI; the import/mount check runs when [fastapi] / [pydantic] is installed. Syntax is always checked |
-o …: exists and is not a directory |
-o takes a directory (it is created if missing) |
Note: The
--styleflag (class/function) is deprecated and maps toclass-json/function-json. It is still accepted in 0.12.0 and will be removed in a future release (see the deprecation policy). Use--templateinstead.
Async Mode (-am / --async-mode)
Controls whether the generated code uses async def / await or plain def:
- Default for JSON templates (
class-json,function-json):yes(async) - Default for Pythonic templates (
pythonic-class,pythonic-builder,pythonic-functional):no(sync)
# Force sync mode on a JSON template
xsm gt machine.json --template class-json --async-mode no
# Force async mode on a Pythonic template
xsm gt machine.json --template pythonic-class --async-mode yes
File Count (-fc / --file-count)
2(default): Generates separate*_logic.pyand*_runner.pyfiles1: Merges everything into a single*.pyfile with de-duplicated imports
# Single merged file
xsm gt machine.json --file-count 1
🧭 Template Selection Guide
Choosing the right template depends on your project needs:
| Criterion | pythonic-class |
pythonic-builder |
pythonic-functional |
class-json |
function-json |
|---|---|---|---|---|---|
| JSON needed at runtime | No | No | No | Yes | Yes |
| Logic in a class | Yes | No | No | Yes | No |
| Type hints | Full | Full | Full | Full | Full |
Decorators (@action, etc.) |
Yes | Yes | Yes | No | No |
| OOP pattern | Subclass | Builder | Functional | Provider | Module |
| Best for large machines | Yes | Yes | Moderate | Yes | Moderate |
| Default async mode | sync | sync | sync | async | async |
Recommendations:
- New projects →
pythonic-class(most Pythonic, no JSON at runtime) - Dynamic assembly →
pythonic-builder(fluent API, easy to extend) - Simple scripts →
pythonic-functional(minimal boilerplate) - Existing JSON workflows →
class-json(keeps JSON as source of truth) - Lightweight / prototyping →
function-json(no class overhead)
🍳 Common Recipes
Sync Mode for Scripts
xsm gt machine.json --template pythonic-class --async-mode no
Generates SyncInterpreter usage instead of Interpreter + asyncio.run().
Single Merged File
xsm gt machine.json --template pythonic-functional --file-count 1
Produces a single machine.py with logic and runner combined. Imports are de-duplicated automatically.
No Logging, No Sleep
xsm gt machine.json --template pythonic-class --log no --sleep no
Generates clean, minimal code without logger.info() calls or time.sleep() between events. Useful for production code where you want to add your own logging.
Force Overwrite + Custom Output Directory
xsm gt machine.json -o ./generated/ --force
Writes to ./generated/ and overwrites any existing files without prompting.
Multiple JSON Files
xsm gt auth.json profile.json settings.json
Generates combined logic and runner files that wire up all three machines. The CLI will interactively ask you to confirm which machine is the parent if it detects invoke configurations.
Using with python -m If xsm Not Found
python -m xstate_statemachine generate-template my_machine.json
python -m xstate_statemachine gt my_machine.json --template pythonic-class
python -m xstate_statemachine # the interactive launcher
This is functionally identical to xsm and works even if the entry point script is not on your PATH. The longer spelling python -m xstate_statemachine.cli still works too.
Windows: “An Application Control policy has blocked this file”
PS> xsm
Program 'xsm.exe' failed to run: An Application Control policy has blocked this file
The one-line fix — run it once, through the interpreter (which the policy already trusts):
python -m xstate_statemachine setup
Scripts dir: C:\Python\Python314\Scripts
xsm.exe: parked as xsm.exe.blocked
xsm.cmd: current
✓ `xsm` now runs through cmd.exe -> python; try `xsm info`
From then on xsm … works in PowerShell and cmd exactly as on any other machine. pip install --upgrade xstate-statemachine recreates xsm.exe, so re-run the same command after upgrading; python -m xstate_statemachine setup --check reports the current state (exit 1 if xsm still resolves to the blocked launcher — handy in a login script), and --undo restores pip’s launcher; --json emits the state for scripts and --scripts-dir DIR targets another interpreter’s Scripts folder (default: the one setup was run from). On macOS and Linux setup is a no-op that says so.
What is happening. pip does not install a Python file called xsm; on Windows it materialises a small, generic, unsigned launcher — Scripts\xsm.exe — that locates python.exe and calls into the package. Machines governed by Windows Defender Application Control (WDAC), AppLocker or Smart App Control allow known signed binaries and block executables that appeared on disk unsigned, so the launcher is refused while Python itself runs fine. Every pip-installed console script (black.exe, pytest.exe, …) is affected in the same way; pipx and uv tool generate the same kind of stub. The launcher is produced by pip on your machine at install time, so nothing in the package can sign or replace it automatically — and a signed standalone xsm.exe would not help either, because a corporate allow-list trusts specific publishers, not “signed by someone”.
What setup does. Two things, both reversible: it renames the blocked xsm.exe to xsm.exe.blocked (Windows resolves .exe before .cmd in the same folder, so the stub has to move aside rather than merely be shadowed), and it writes xsm.cmd beside it — a batch file that runs python -m xstate_statemachine with the interpreter setup was run from, falling back to python on PATH. Batch files execute through the trusted cmd.exe, which these policies allow. Verified on a WDAC-managed machine from both PowerShell and cmd.
If even that is refused (a policy that also enforces script rules) you are in a fully managed environment: use python -m xstate_statemachine … directly — it is functionally identical to xsm and xsm info prints the exact spelling on its Also run as: line — or add a PowerShell function to your $PROFILE:
Add-Content $PROFILE 'function xsm { python -m xstate_statemachine @args }'
🛠️ Workflow: From Design to Running Code
The recommended workflow integrates the CLI into a design-first approach:
flowchart LR
A["🎨 Design<br/><small>Stately.ai editor or JSON by hand</small>"] --> B["📦 Export JSON<br/><small>XState config</small>"]
B --> C["⚙️ Generate<br/><small>xsm gt machine.json</small>"]
C --> D["✍️ Implement<br/><small>fill TODO stubs in _logic.py</small>"]
D --> E["🚀 Run<br/><small>python machine_runner.py</small>"]
E -. iterate .-> A
🚶 Complete Walkthrough: From JSON to Running Machine
Let’s walk through the entire process end-to-end.
Step 1: Create or Export Your JSON Config
Save this as checkout.json:
{
"id": "checkout",
"initial": "cart",
"context": {
"items": [],
"total": 0
},
"states": {
"cart": {
"on": {
"SUBMIT": {
"target": "payment",
"guard": "cartNotEmpty",
"actions": "calculateTotal"
}
}
},
"payment": {
"invoke": {
"src": "processPayment",
"onDone": {
"target": "confirmed",
"actions": "clearCart"
},
"onError": {
"target": "cart",
"actions": "showError"
}
}
},
"confirmed": {
"type": "final"
}
}
}
Step 2: Generate Python Code
xsm gt checkout.json --template pythonic-class --async-mode no
Output:
Generated logic file: checkout_logic.py
Generated runner file: checkout_runner.py
Step 3: Review the Generated Logic File
Open checkout_logic.py and you’ll find a StateMachine subclass with:
State()declarations forcart,payment, andconfirmed- Transition definitions with guard and action references
@actiondecorated methods forcalculateTotal,clearCart, andshowError@guarddecorated method forcartNotEmpty@servicedecorated method forprocessPayment
Stately exports and other non-identifier names. Stately names anonymous actions like
inline:checkout.payment#entry[0]; hand-written configs sometimes use dots or dashes (audit.log-v2). Such a name cannot become a Python method name and still be matched back by convention, so the generator emits the explicit form for it:@action("inline:checkout.payment#entry[0]") def inline_checkout_payment_entry_0(self, interpreter, context, event, action_def) -> None: ...The
LogicLoaderhonours that declared name, socreate_machine(config, logic_providers=[...])andlogic_modules=[...]bind it correctly. Rename the method freely; keep the string in the decorator. Ordinary camelCase names keep the bare@action– the method name round-trips on its own.
Step 4: Implement Your Business Logic
Fill in the # TODO stubs with your actual business logic:
@guard
def cart_not_empty(self, context, event) -> bool:
"""Guard: cartNotEmpty — check if the cart has items."""
return len(context.get("items", [])) > 0
@action
def calculate_total(self, interpreter, context, event, action_def) -> None:
"""Action: calculateTotal — sum up item prices."""
context["total"] = sum(item["price"] for item in context["items"])
@service
def process_payment(self, interpreter, context, event) -> dict:
"""Service: processPayment — charge the customer."""
# Call your payment gateway here
return {
"result": "payment_success",
"transaction_id": "txn_123",
}
Step 5: Run the Machine
python checkout_runner.py
The runner will start the interpreter, send the SUBMIT event, invoke the payment service, and transition through the machine states with full logging output.
Tip: After initial generation, you only edit the logic file. The runner file rarely needs changes unless you want custom event sequences.
📋 All Commands Overview
xsm [-h] [-v] [--plain] [--no-color] [--no-anim] [--verbose]
{generate-template,gt,list-templates,lt,validate,val,paths,coverage,info,
plugins,update,snapshots,dlq,asyncapi,setup,inspect,ins,replay,diagram,
dia,simulate,sim,docs,new} ...
| Command | Alias | Description |
|---|---|---|
| (none) | — | Interactive launcher — menu, recent files, generate wizard (terminal only) |
generate-template |
gt |
Generate Python code from an XState JSON file |
list-templates |
lt |
List all available code generation templates |
validate |
val |
Build each file with the real library and list findings |
inspect |
ins |
State tree, transitions table, logic to implement, policies |
simulate |
sim |
Run a machine live, or replay a scripted event sequence |
paths |
— | A path to every reachable configuration, found by running the real engine (--simple, --guards both) |
diagram |
dia |
Mermaid / PlantUML / ASCII diagram to stdout or a file |
docs |
— | A Markdown reference page per machine |
info |
— | Version, environment, feature cards, links |
update |
— | Check PyPI and upgrade to the latest release with the installer that installed you (--check, --yes) |
setup |
— | Windows: swap pip’s blocked xsm.exe launcher for a batch shim (--check, --undo, --json, --scripts-dir) |
plugins |
— | List the installed plugin / store / broker entry points and the hooks each implements (--json, --strict) |
replay |
— | Print or stream (--live, --speed) a recording written by xsm simulate --record (see Live inspector) |
new |
— | Scaffold a project from an example app (--template fastapi, --list) |
snapshots |
— | Ops view of a persistence store: keys, labels, status, age; --stale is the drain list for a deploy, --fail-if-stale the gate |
coverage |
— | Render a statechart coverage report written by pytest --xsm-coverage and gate on it (--fail-under) |
dlq |
— | List / show / replay / purge dead-lettered messages (see Event-driven architecture). Exit codes: 0 ok, 1 a real replay ran and did not succeed, 2 refused input (missing / non-SQLite store file, unknown id, bad --older-than, unloadable --machine / --logic, missing --yes / --reason, a refused replay); errors are one stderr line |
asyncapi |
— | An AsyncAPI 3.0 document from a chart (-o, --validate); exit 0 ok, 2 refused input (a missing or invalid machine file, an empty --server / --inbound / --outbound, an unwritable -o, --validate without jsonschema or a document the schema rejects); errors are one stderr line |
Every command that reports facts also has a --json switch (validate, inspect, paths, simulate, list-templates, info, snapshots, dlq, coverage) so the same information can be consumed by scripts.
🧭 Interactive Launcher
xsm
On a terminal, a bare xsm draws the banner and a menu:
What would you like to do?
❯ Generate code Pick JSON files and a template; preview before writing
Inspect a machine State tree, events, logic and policies at a glance
Simulate Run a machine live: pick events, advance the clock
Validate Build files with the real library and list findings
Diagram Mermaid / PlantUML / ASCII to stdout or a file
Docs A Markdown reference page per machine
Templates Browse the code generation catalogue
About Version, environment, links
Update Check PyPI and upgrade to the latest release
New project Scaffold a FastAPI project from the example
Quit
↑↓ move · enter select · esc cancel
Arrow keys or the digits 1–9 move, enter selects, esc backs out; Ctrl+C anywhere exits quietly with status 130. File pickers list recently used machines first (remembered in ~/.xsm/recent.json, or $XSM_HOME/recent.json); the last entry lets you type a path or a glob such as machines/*.json. Pasted input is normalised: the surrounding quotes from Explorer’s Copy as path (or a dragged file) are stripped, ~ is expanded, a file:/// URI is accepted, and a directory means its *.json files.
The generate wizard asks for files, a template, companions and options, then shows the head of the logic module it is about to write in a preview panel before asking for confirmation. It builds exactly the same argument set the gt command uses, so nothing the wizard produces differs from the flag-driven output.
When stdout is not a terminal a bare xsm prints the usual argparse error asking for a subcommand — the launcher never engages inside a pipe.
🔍 Inspect
xsm inspect checkout.json
xsm ins checkout.json --no-events # skip the transitions table
xsm ins checkout.json --json # machine-readable facts
Builds the machine with the real library and lays out what it found:
╭ inspect ─────────────────────────────────────────────────────╮
│ checkout.json │
├──────────────────────────────────────────────────────────────┤
│ Machine checkout │
│ States 5 atomic 5 │
│ Events 7 Timers 1 Invokes 1 │
│ Logic 5 actions · 2 guards · 1 services · 0 named delays │
╰──────────────────────────────────────────────────────────────╯
─────────────────────────── state tree ───────────────────────────
◆ checkout initial=editing
├── ○ authenticating3DS invoke authenticatePayment
├── ○ challenge after 2000
├── ○ editing
├── ○ failure
╰── ◉ success
──────────────────────── transitions (11) ────────────────────────
┌─────────────┬───────────────────┬───────────────────┬─────────────┬───────────┐
│ Event │ From │ To │ Guard │ Actions │
├─────────────┼───────────────────┼───────────────────┼─────────────┼───────────┤
│ SUBMIT │ editing │ authenticating3DS │ isFormValid │ │
│ after 2000 │ challenge │ success │ │ │
│ RESET │ challenge │ editing │ │ resetForm │
│ … │ │ │ │ │
└─────────────┴───────────────────┴───────────────────┴─────────────┴───────────┘
──────────────────────── logic to implement ──────────────────────
Kind │ Names
actions │ resetForm, …
guards │ isFormValid, …
services │ authenticatePayment
──────────────────────────── policies ────────────────────────────
actionErrorPolicy │ continue
guardErrorPolicy │ false
onUnhandled │ ignore
maxIterations │ 1000
strict │ false
strictTargets │ false
event schemas │ none
The tree marks state kinds (○ atomic, ◆ compound, ⫴ parallel, ◉ final, ↺ history) and annotates timers and invokes; the Guard column shows composite guards as the library resolved them. Unreachable states — no transition, initial or history target leads to them — are reported as a warning, as is anything the library itself logged while building.
🗺️ Paths
xsm paths checkout.json # one shortest path per reachable configuration
xsm paths checkout.json --guards both # also explore guard=False / service-error branches
xsm paths checkout.json --simple --max-paths 50
xsm paths checkout.json --json
xsm paths checkout.json --weight time # least simulated clock time, not fewest steps
checkout 5 paths (shortest, guards=both)
+---------------------+------------------+-----------------------------+
| Configuration | Events | Assumes |
+---------------------+------------------+-----------------------------+
| checkout.cart | (initial) | |
| checkout.paying | CHECKOUT | |
| checkout.paid | CHECKOUT,PAY | |
| checkout.declined | CHECKOUT,PAY | service:charge=error |
| checkout.timedOut | CHECKOUT,+30000 | |
+---------------------+------------------+-----------------------------+
The Events column is the xsm simulate --events grammar — paste it back into xsm sim to reproduce the path interactively. Assumes lists what the path relies on when --guards both explores alternatives: a guard forced False, a service forced to fail, or a named delay with no implementation. Every step was executed by the real engine (stub logic, simulated clock), never inferred from the JSON — see Path generation for the Python API.
--max-depth (default 50) and --max-paths (default 1000, --simple only) must be >= 0. Exit codes: 0 success, 1 the chart does not build or cannot be explored (one line, no traceback), 2 bad arguments. xsm simulate --events rejects a clock advance that is not a finite, non-negative number (+-5, +nan, +1e309) with exit code 2.
📊 Coverage
xsm coverage FILE [--fail-under N] [--plain] [--json] renders the version-1 JSON report that pytest --xsm-coverage --xsm-coverage-report=json:FILE writes (see State & transition coverage): one row per machine with visited/total states and hit/total transitions, then each machine’s unvisited states and unhit transitions. --json re-emits the document. --fail-under N applies to both state and transition coverage; N must be a finite number in [0, 100].
pytest --xsm-coverage --xsm-coverage-report=json:cov.json
xsm coverage cov.json --plain --fail-under 90
| Exit | Meaning |
|---|---|
0 |
rendered; every machine at or above --fail-under (or no threshold) |
1 |
a machine is under --fail-under; the report has no machines and a threshold was given; or the file is missing / not JSON / not version: 1 (one line, no traceback) |
2 |
bad arguments (--fail-under nan, 101, -1, abc) |
🎮 Simulate
One engine drives two modes: a SyncInterpreter on a SimulatedClock with stub logic (actions are recorded, guards return True unless you say otherwise, services are no-ops), an undo stack of snapshots, and a step history.
Live
xsm simulate checkout.json
On a terminal you get the status panel and state tree, then a picker of the events that would do something right now (denied events are never offered):
↑↓ pick event · enter send · t +timer · c +clock · g guards · u undo · r reset · h history · s snapshot · q quit
Send
❯ SUBMIT
UPDATE_FORM
enter send · esc for commands
When the active state has no sendable event — a chart whose transitions are all always / after, or a final state — the picker is skipped and a ❯ no event can be sent from here line shows the command keys instead; ↑/↓/enter there print a hint rather than doing nothing. enter sends the highlighted event; a step line shows what happened (→ SUBMIT authenticating3DS actions: validate), the new active states pulse briefly, and the panel redraws. esc drops into command mode:
| Key | Does |
|---|---|
t |
Fire the longest armed after timer in the active configuration (advances the clock past it) |
c |
Advance the clock by a number of milliseconds you type |
g |
Toggle which guards return True — a multiselect over every guard name |
u |
Undo the last step (restores the snapshot; after timers are re-armed from zero) |
r |
Reset to the initial state with a fresh clock; clears the undo stack |
h |
Print the step history as a table (step, result, actions, clock) |
s |
Print the current snapshot JSON |
q / esc |
Quit |
Scripted (CI)
# events in order; '+500' advances the clock 500 ms
xsm sim checkout.json --events SUBMIT,+2001
# force a guard to False, then export the run
xsm sim checkout.json -e SUBMIT --guards-false isFormValid --json
# replay a script file
xsm sim checkout.json --script steps.json --json
--events / --clock / --script / --json — or simply not having a terminal — put the command in scripted mode. It replays the commands, prints each step and the final state, and exits 0 (or 1 if the file does not build). The script file is a JSON list of {"send": "GO"}, {"send": "GO", "payload": {...}}, {"clock": 500}, {"guard": "g", "value": false}, {"undo": true}, {"reset": true}. A malformed script — not a list, a step without send / clock / guard / undo / reset, a non-object payload, a negative or non-numeric clock, a non-boolean value — is one error line naming the step, exit 2. The file is parsed as JSON only; nothing in it is evaluated. --script cannot be combined with --events / --clock (exit 2): a replay runs exactly the recorded steps. --guards-false sets the starting value of those guards; a script’s {"guard": …} step overrides it from that step on. A model_test failure writes exactly this format, so its artefact replays with xsm sim chart.json --script failing.json. (--record is different: it writes an Inspector JSON Lines file for xsm replay, not a script.)
→ SUBMIT challenge
⏱ +2001 ms success
╭ simulate ───────────────────────────────────────────╮
│ checkout │
├─────────────────────────────────────────────────────┤
│ status running clock 2001 ms steps 2 │
│ context {"amount": 149900, "cardHolder": ""} │
╰─────────────────────────────────────────────────────╯
The --json document carries active, value, context, clock_ms, enabled_events, chain_trips and history (one record per step with before, after, changed, actions, denied, deferred, error), so a CI job can assert on a run without parsing text.
🖼️ Diagram
xsm diagram checkout.json # Mermaid to stdout
xsm dia checkout.json -f plantuml
xsm dia checkout.json -f ascii
xsm dia checkout.json -f mermaid -o docs/ # writes docs/checkout.mmd
mermaid and plantuml reuse the library’s own exporters (to_mermaid, to_plantuml), so what the CLI draws is exactly what machine.to_mermaid() would give you. ascii prints the state tree followed by one from --EVENT [guard] / actions-> to line per transition. With -o pointing at a directory the file is named after the machine id with the format’s extension (.mmd, .puml, .txt).
📝 Docs
xsm docs checkout.json # Markdown to stdout
xsm docs machines/*.json -o docs/ # one <machine-id>.md per file
A file that does not build is reported and skipped; the remaining pages are still written and the command exits 1 at the end. Each page has the summary line, an embedded Mermaid diagram, the state tree, the transitions table, the logic to implement, the policies and a “Getting started” snippet that loads the JSON and binds a MachineLogic — a reference page you can drop into a docs site or a PR.
📃 List Templates
Shows all available code generation templates with descriptions:
xsm list-templates
# or
xsm lt
xsm lt --json
Output (plain mode):
Available code generation templates
--------------------------- Logic modules that load the JSON at runtime ----------------------------
Template ID | Style | Description
class-json | Class + JSON | OOP logic class with MachineLogic, bound to a JSON config…
function-json | Functions + JSON | Module-level functions with LogicLoader auto-discovery, J…
---------------- Pure-Python re-expressions of the machine (structurally verified) -----------------
Template ID | Style | Description
pythonic-class | Class-Based | StateMachine subclass with @action, @guard, @service decor…
pythonic-builder | Builder Pattern | Fluent MachineBuilder API for dynamic, programmatic machin…
pythonic-functional | Functional | Simple build_machine() call with explicit state and transi…
------------------------- Companion outputs -- add alongside any template --------------------------
Template ID | Style | Description
pytest | Test scaffold | A pytest module per machine: initial state, every r…
typed | Typed context + events | TypedDict for context, Literal alias for event name…
plugin | Plugin skeleton | A PluginBase subclass wired for exactly the hooks t…
fastapi-router | FastAPI router | An editable APIRouter: GET state + one typed POST p…
pydantic-models | Pydantic models | A context BaseModel inferred from the chart plus on…
Feature support
+----------------------+---------------+------------+--------------------------+
| Template ID | Machine built | Verified | Config needed at runtime |
+----------------------+---------------+------------+--------------------------+
| class-json | from JSON | syntax | yes -- ship the .json |
| function-json | from JSON | syntax | yes -- ship the .json |
| pythonic-class | in Python | structural | no |
| pythonic-builder | in Python | structural | no |
| pythonic-functional | in Python | structural | no |
| pytest | n/a | compiles | no |
| typed | n/a | compiles | no |
| plugin | n/a | compiles | no |
| fastapi-router | n/a | compiles | no |
| pydantic-models | n/a | compiles | no |
+----------------------+---------------+------------+--------------------------+
All templates support nesting, parallel regions, history, guards,
timers (numeric and named delays), invoke, tags and meta.
'Verified' is what the generator proves before writing: templates that
build the machine in Python are executed and compared against the source.
Usage: xsm generate-template <file.json> --template <template-id>
xsm gt <file.json> -t pythonic-class --with-types --with-tests
📡 Live inspector
[0.11.0] Watch a machine live in the browser — in the Stately Inspector or the built-in fallback page — speaking the @statelyai/inspect protocol (guide, #274).
xsm inspect machine.json --live --open # serve + interactive simulator
xsm inspect machine.json --live -e SUBMIT,+2000 --duration 60 # scripted
xsm sim machine.json -e SUBMIT,+2001 --record session.jsonl # record (file is 0600)
xsm replay session.jsonl # print the recording
xsm replay session.jsonl --live --speed 1 # stream it at recorded pace
| Flag | Meaning |
|---|---|
--live |
serve the stream (stdlib SSE over http.server) instead of printing the machine facts |
--port N |
default 8765; 0 picks a free port |
--host H |
default 127.0.0.1; any non-loopback host requires --token |
--token T |
default: a fresh secrets.token_urlsafe(32) per run |
--open |
open the printed URL in a browser |
--context a,b |
context keys the page may see — none by default (also on sim --record) |
-e / --duration |
with --live: run a scripted session and serve for N seconds |
--speed |
replay --live: 1.0 = recorded pace, 2.0 = twice as fast, 0 = all at once; negative is refused (exit 2) |
--append |
sim --record: add to an existing recording (without it, a non-empty file is refused with exit 2) |
Exit codes: 2 for a refused option (non-loopback --host without --token, negative --speed, existing recording without --append), 1 for a missing or corrupt file or a port that cannot be bound, 130 on Ctrl-C. xsm replay streams the file, so a large recording prints from the first line. --open on a machine without a browser only warns.
The printed URL carries the token once: the first load answers with an HttpOnly; SameSite=Strict cookie and redirects to /, so the token does not stay in the address bar. curl works with -H "X-XSM-Token: …":
curl -sN -H "X-XSM-Token: $TOKEN" http://127.0.0.1:8765/events
✅ Validate
Builds each file with the real library and reports what it found:
xsm validate my_machine.json
# or
xsm val my_machine.json
# Validate multiple files at once
xsm val auth.json profile.json settings.json
# Unknown config keys as warnings instead of errors
xsm val legacy.json --lenient
# Findings as JSON (exit code is still 1 when any file fails)
xsm val machines/*.json --json
Validation is not a schema check — the file is passed through create_machine(strict_config=True) with stub logic, so it catches exactly what the library would refuse at runtime: bad JSON, missing initial, unresolvable targets, misspelled keys (with the library’s “did you mean” hints), and so on. The report also lists states no transition can reach, and every warning the library logged while building.
Example output for a valid file:
ok checkout.json
Machine: checkout
States: 3
Events: 3
Actions: calculateTotal, clearCart, showError
Guards: cartNotEmpty
Services: processPayment
OK All 1 file(s) are valid.
A failing file prints its findings under the file name and the command exits 1.
ℹ️ Info
Displays library version, Python version, platform, and a feature summary:
xsm info
xsm info --json
The page opens with the banner and an environment panel:
Version: 0.12.0
Python: 3.12.0
Platform: Windows-11
Install path: C:\...\xstate_statemachine
followed by feature cards (interpreters, XState compatibility, Pythonic API, hierarchy and parallel regions, actors, plugins, snapshots, diagrams, the CLI itself) and the documentation, PyPI and GitHub links.
⬆️ Update
xsm update # check PyPI, ask, upgrade
xsm update --check # report only; exit 1 if a newer release exists
xsm update --yes # no confirmation (scripts, CI images)
xsm update --json
Installed: 0.11.0
Latest: 0.12.0 (update available)
Install: pip C:\Python\Python314\Lib\site-packages
Upgrade to 0.12.0 with `python.exe -m pip install --upgrade xstate-statemachine`? [y/N] y
…pip output…
✓ updated xstate-statemachine 0.11.0 -> 0.12.0
update asks PyPI’s JSON API for the latest non-prerelease version (stdlib urllib, 10 s timeout — offline it says so and exits 1) and then upgrades with the tool that installed this copy, because using the wrong one corrupts an environment:
| How you installed | What update runs |
|---|---|
pip install (incl. uv pip in a venv) |
<this python> -m pip install --upgrade xstate-statemachine |
pipx install |
pipx upgrade xstate-statemachine |
uv tool install |
uv tool upgrade xstate-statemachine |
editable checkout (pip install -e .) |
refuses — a development checkout is updated with git |
| conda environment | refuses — prints conda update xstate-statemachine |
| anything else | refuses — prints the manual pip command |
The installer runs with your terminal attached so you see its own output. Afterwards update asks a fresh interpreter for --version to confirm the result (the running process still has the old module loaded). On Windows, if the setup shim was in place, update re-applies it automatically — pip’s upgrade recreates the blocked xsm.exe, and without this the machine that needed setup would break right after updating.
Off a terminal without --yes, update prints the command it would run and exits 1 rather than changing anything.
On Windows, when you run xsm update through pip’s xsm.exe launcher, that launcher is the parent of the update process and stays alive until it finishes — so pip cannot delete it and would fail with WinError 32: The process cannot access the file because it is being used by another process. update detects this and hands the job to a detached python -m xstate_statemachine update --yes process (it prints handing over to …), which runs pip after the launcher has exited. You see pip’s output and the final ✓ updated … line as usual.
❓ Version and Help
# Show version
xsm --version
# Show help
xsm --help
xsm generate-template --help
Verification: The Generator Refuses to Lie
Since v0.7.0, xsm proves its output before writing it. For templates that
build the machine in Python (pythonic-class, pythonic-builder,
pythonic-functional) it:
- compiles the generated module,
- executes it and builds the machine,
- compares that machine structurally against
create_machine(your.json).
If anything diverges, nothing is written and the command exits 1:
❌ Refusing to generate 'pythonic-builder' code for machine 'orders'.
The generated code would not faithfully reproduce the source machine:
• state 'processing.payment' is missing from the generated code
Nothing was written. This is deliberate: emitting a machine that silently
differs from its source is worse than emitting nothing.
The *-json templates load your JSON at runtime, so their fidelity is exact by
construction — they get syntax validation only.
Use --no-verify to inspect output the generator refuses to write. It does not
disable syntax checking.
[!WARNING] Verification executes the generated code in-process. That is what makes the guarantee meaningful — proving the code builds the right machine means building it — but it means
xsmruns code derived from your JSON with your privileges.Untrusted JSON cannot inject code: every value is emitted through
repr(), and text reaching a docstring is stripped of quotes, backslashes and newlines. The scratch module is never registered insys.modules, andsys.modulesis restored afterwards.Even so, if you are generating from a machine definition you do not trust,
--no-verifyskips the execution step. You lose the fidelity guarantee and keep the syntax check.
Why this exists: before v0.7.0 nothing checked. Three templates shipped code that produced a different machine than the source described, and two of them did it silently with exit code 0. See the changelog for the details.
Keeping Generated Code Honest in CI
Generated code is often committed so reviewers can see it and consumers don’t need the CLI. The risk is drift: someone edits the machine, forgets to regenerate, and the repository now describes a machine that no longer exists.
--check catches that. It regenerates in memory and compares:
xsm generate-template order.json --template pythonic-builder --check
✓ Generated code is up to date.
Exit code is 1 when anything differs or is missing. Use --diff to see exactly
what changed:
xsm generate-template order.json --template pythonic-builder --diff
Both flags are strictly read-only — they never write, and never prompt for overwrite confirmation, so they are safe in a non-interactive pipeline.
GitHub Actions example
- name: Verify generated machine code is current
run: |
xsm generate-template machines/order.json \
--template pythonic-builder \
--output src/machines \
--check
In CI and pre-commit
The xsm-check GitHub Action
The repository is itself a composite action. It installs the library, runs xsm validate --plain on every file matching files, and — when generated-dir is set — xsm gt --check against that directory. The @v0.12.1 / rev: v0.12.1 pins below name the release this guide documents; the tag exists from that release onward (pin @main or a commit SHA to track an unreleased tree):
- uses: actions/checkout@v4
- uses: basiltt/xstate-statemachine@v0.12.1 # pin a tag (or a commit SHA)
with:
files: "machines/**/*.machine.json" # bash globstar pattern
generated-dir: src/machines # optional
gt-args: "-t pythonic-builder" # the flags you generated with
python-version: "3.13" # optional
package: "xstate-statemachine==0.12.1" # optional: pin the CLI too
The action fails when no file matches, when any machine has an error, or when the generated code is stale. The repository runs it on the fastapi_orders example in .github/workflows/xsm-check-selftest.yml.
pre-commit hooks
.pre-commit-hooks.yaml publishes two hooks:
| Hook | Runs | On |
|---|---|---|
xsm-validate |
xsm validate --plain <staged files> |
files ending in machine.json (so order.machine.json and machine.json) |
xsm-gt-check |
xsm gt --check --plain <args> |
once per commit (pass_filenames: false); args is required — the input JSON, -o and the template flags you generate with |
repos:
- repo: https://github.com/basiltt/xstate-statemachine
rev: v0.12.1
hooks:
- id: xsm-validate
- id: xsm-gt-check
args: [machines/order.machine.json, -o, src/machines, -t, pythonic-builder]
Both hooks are language: python, so pre-commit installs the library into its own environment; nothing is needed on your PATH.
Plugins
xsm plugins # name, distribution, version, group and the PluginBase hooks each implements
xsm plugins --json
xsm plugins --strict # exit 1 if any entry point fails to load (a CI gate)
Loads the entry points declared under the xstate_statemachine.plugins, .stores and .brokers groups. Listing imports them, so set XSM_DISABLE_PLUGIN_DISCOVERY=1 to disable discovery (the security page explains why). Writing and registering a plugin is covered in Plugins.
Dead letters (xsm dlq)
xsm dlq --dlq sqlite:///dlq.db <command> operates a dead-letter store. --dlq also accepts a plain file path.
| Command | Flags |
|---|---|
list |
--all (include resolved), --limit N, --json |
show RECORD_ID |
prints one record as JSON |
replay RECORD_ID |
--store URL, --machine FILE (repeatable), --logic MODULE, --dry-run (default) / --no-dry-run, --yes, --reason TEXT, --force (replay although the machine changed since capture), --json |
purge |
--id RECORD_ID, --older-than 30d (12h, 15m), --yes, --reason TEXT, --json |
replay is a dry run unless given --no-dry-run --yes --reason; purge needs --yes --reason. Exit codes are in the All Commands table, and the model behind it is in Event-driven architecture.
AsyncAPI (xsm asyncapi)
xsm asyncapi machine.json -o asyncapi.json --server localhost:9092 --protocol kafka --validate
xsm asyncapi machine.json --inbound orders.in --outbound orders.out
Writes an AsyncAPI 3.0.0 document with CloudEvents messages: the chart’s on keys are the consumed events, meta.publish transitions and publish-tagged states the published ones. --server is the broker host, --protocol one of kafka, kafka-secure, amqp, amqps, nats, redis, sqs, sns, mqtt, http, https, ws, wss, pulsar, googlepubsub, jms, solace; --inbound / --outbound set the channel addresses; --validate checks the result against the vendored schema (needs jsonschema). More in Event-driven architecture.
Snapshots
xsm snapshots --store sqlite:///orders.db # every key: label, status, age, deadlines
xsm snapshots --store sqlite:///orders.db machine.json --stale # keys whose label differs from the chart's
xsm snapshots --store sqlite:///orders.db machine.json --fail-if-stale # exit 1 while any remain -- a deploy gate
xsm snapshots --store file:///var/lib/app/state --prefix order. --limit 50 --json
What is in a store, and what a new chart revision still has to migrate. --store takes sqlite:///relative.db, sqlite:////absolute.db (SQLAlchemy’s rule; sqlite:///C:/... works on Windows) or file:///dir; a path that does not exist is refused with exit 2 rather than silently created empty, and memory:// is refused as meaningless. With a machine JSON the chart’s "version" is the reference; --stale lists the keys whose stored machine_version differs. Two rules agree with restore: an unlabelled record (written before the chart had a "version", or by 0.10.x) is not stale — a restore warns, it does not refuse — and a chart with no "version" has nothing stale.
Cheap at scale: the label index is read for every key (list_versions — one SELECT on SQLite, a header read on FileStore), never the snapshot blobs, and --limit caps the output, not the scan — 10 000 keys with 100 stale take 0.1 s on SQLite. --json carries total and truncated when the limit cut the list; the table title says “N of M”. Keys are shown with control characters escaped so a key containing a newline cannot fake a row (--json keeps them verbatim). --json never includes context (X0.1). Exit codes: 0 listed (stale or not), 1 --fail-if-stale and stale keys exist, 2 bad input — not a machine, a missing or unreadable store, a machine file over 16 MiB or nested past the parser.
Pair it with the migrator: deploy readers that carry the SnapshotMigrator, let traffic migrate each instance on its first write, and run --fail-if-stale before retiring the old workers. See Versioning in-flight instances and the fastapi_orders rolling upgrade. Django models have the same view as manage.py xsm_snapshots app.Model [--stale] [--json].
New project
xsm new --list # templates and their status
xsm new my_service # --template fastapi, --name orders
xsm new my_service --name shop_orders --force # write into a non-empty dir
xsm new --template fastapi DIR copies the fastapi_orders example into DIR: machine.json, models.py, logic.py, app.py, static/, tests/, a README.md and a requirements.txt pinning xstate-statemachine[fastapi]. --name (lower_snake_case, default orders) becomes the URL prefix, the store prefix and — camelCased — the machine id. Templating is the standard library’s string.Template; there is no cookiecutter dependency. A non-empty DIR is refused unless you pass --force. xsm new --template flask DIR does the same with the flask_wizard example (pinning xstate-statemachine[flask]). There is no django template: the django_approvals example is a whole Django project – copy it; --template django refuses and prints that URL. Exit status 2 on any refusal.
cd my_service && pip install -r requirements.txt && python -m pytest tests -q
For the Flask scaffold (--name only fills the README title; the machine stays wizard):
xsm new --template flask my_wizard
cd my_wizard && pip install -r requirements.txt
flask --app app run # http://127.0.0.1:5000/
python -m pytest tests -q
flask xsm (Flask apps)
With [flask] installed, XState.init_app(app, ..., cli=True) (the default) adds a flask xsm command group. inspect, diagram, docs and simulate take a registered machine name instead of a file path and accept the same flags as the xsm commands; the JSON comes from the registration (source= when you registered a MachineNode).
flask --app app xsm inspect wizard --plain
flask --app app xsm diagram wizard -f mermaid
flask --app app xsm simulate wizard -e NEXT -e BACK
From the flask_wizard example (an excerpt of inspect --plain; the test suite checks these lines):
+ wizard initial=account
|-- o account
|-- o profile
|-- o plan
|-- o confirm
`-- @ done
| NEXT | account | profile | hasAccount | saveAccount |
| SUBMIT | confirm | done | | |
manage.py xsm_* (Django projects)
With [django] installed and xstate_statemachine.contrib.django in INSTALLED_APPS, the same reports are management commands. They take a model label (app.Model, a StatechartModelMixin model) instead of a file path. A chart given as a dict or a callable is written to a temporary JSON file first.
python manage.py xsm_inspect approvals.Expense --plain
python manage.py xsm_inspect approvals.Expense 17 # plus row 17's state and version
python manage.py xsm_diagram approvals.Expense -f mermaid
python manage.py xsm_simulate approvals.Expense -e SUBMIT,LEGAL_APPROVE
python manage.py xsm_snapshots approvals.Expense --stale --json
python manage.py xsm_migrate_fsm legacy.Ticket --field state --dry-run > ticket.json
python manage.py xsm_migrate_fsm legacy.Ticket --field state --map open=new --batch 500
xsm_migrate_fsm moves a django-fsm-2 model over (recipe): --dry-run prints only the chart JSON on stdout (the recipe goes to stderr); without it, it fills empty snapshots and reports per value, with row counts, any column value the chart does not know; --map OLD=NEW folds such a value into a state.
xsm_inspect, xsm_diagram, xsm_docs and xsm_simulate print byte for byte what xsm inspect / diagram / docs / simulate <chart.json> prints. xsm_simulate only runs the -e events and never reads stdin. The full table, with xsm_deadlines, xsm_refresh_columns and xsm_migrate_fsm, is in Django → Management commands. Exit status: 0, 1 (CommandError: unknown model, missing pk, bad --database, unwritable -o / --write-chart, bad --batch / --map), 2 (bad flag).
Reading Generated Files
Every generated file starts with a provenance header:
"""Generated state machine logic — DO NOT EDIT BY HAND.
Source: order.json
Template: pythonic-builder
Generator: xstate-statemachine 0.12.0
Regenerate with::
xsm generate-template order.json --template pythonic-builder
Implement your logic in the stubs below; the machine structure
above is derived from the source JSON and will be overwritten.
"""
The machine structure is derived from your JSON and will be overwritten on regeneration. Your action, guard and service bodies are the parts you own — keep them in the logic file and treat the runner as disposable.
Generated code passes black --check and pyflakes cleanly, so it will not
add lint noise to your project — provided the formatters are installed:
pip install "xstate-statemachine[format]"
The core library has zero runtime dependencies, so black and isort are
not pulled in by default. Without them the generated code is still valid and
still faithful to your machine — it simply is not line-wrapped, and xsm says
so once per run.