CLI Code Generator
Generate production-ready Python from XState JSON with the xsm command.
CLI Code Generator
The xsm CLI tool generates production-ready Python code from XState JSON machine configurations. Instead of manually writing boilerplate action stubs, guard functions, service handlers, and interpreter bootstrap code, you export your machine from Stately.ai (or write JSON by hand) and let xsm scaffold everything for you.
The generated code includes type hints, docstrings, error handling, logging, and is ready to run immediately.
π€ What Is the CLI?
The CLI is a code generator that reads one or more XState-compatible JSON machine definitions and produces Python source files. It bridges the gap between visual state machine design and production Python code.
What it does NOT do: The CLI does not run your state machine. It generates the Python files that you then execute with python.
π Basic Usage
# Generate 2 files: logic + runner (default)
xsm generate-template my_machine.json
# Short alias β identical behavior
xsm gt my_machine.json
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_statemachine.cliinstead. See CLI Troubleshooting 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) |
-t |
--template |
CHOICE |
class-json |
Code generation template (see below) |
-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; exit 1 if files on disk are out of date |
| β | --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 templates are available:
| 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 |
Note: The
--styleflag (class/function) is deprecated and maps toclass-json/function-json. It is still present in 0.8.0 and will be removed in a future release. 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.cli generate-template my_machine.json
python -m xstate_statemachine.cli gt my_machine.json --template pythonic-class
This is functionally identical to xsm and works even if the entry point script is not on your PATH.
π οΈ 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
The xsm CLI provides four commands:
xsm [-h] [-v]
{generate-template,gt,list-templates,lt,validate,val,info} ...
| Command | Alias | Description |
|---|---|---|
generate-template |
gt |
Generate Python code from an XState JSON file |
list-templates |
lt |
List all available code generation templates |
validate |
val |
Validate an XState JSON config file |
info |
β | Show library version, Python version, and feature summary |
π List Templates
Shows all available code generation templates with descriptions:
xsm list-templates
# or
xsm lt
Output:
Available code generation templates:
Template ID Style Description
----------------------- ------------------- -------------------------------------------------------
class-json Class + JSON OOP logic class with MachineLogic, bound to a JSON config loaded at runtime.
function-json Functions + JSON Module-level functions with LogicLoader auto-discovery, JSON config at runtime.
pythonic-class Class-Based StateMachine subclass with @action, @guard, @service decorators. Pure Python.
pythonic-builder Builder Pattern Fluent MachineBuilder API for dynamic, programmatic machine construction.
pythonic-functional Functional Simple build_machine() call with explicit state and transition definitions.
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
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>
β Validate
Validates that JSON files are well-formed XState machine configurations:
xsm validate my_machine.json
# or
xsm val my_machine.json
# Validate multiple files at once
xsm val auth.json profile.json settings.json
The validator checks:
- Valid JSON syntax
- Required fields (
id,initial,states) - State structure and transitions
- Reports all actions, guards, and services found in the config
Example output for a valid file:
ok checkout.json
Machine: checkout
States: 3
Actions: calculateTotal, clearCart, showError
Guards: cartNotEmpty
Services: processPayment
All 1 file(s) are valid.
βΉοΈ Info
Displays library version, Python version, platform, and feature summary:
xsm info
Example output:
XState-StateMachine CLI
----------------------------------
Version: 0.8.0
Python: 3.12.0
Platform: Windows-11
Install path: C:\...\xstate_statemachine
Features:
* Async + Sync interpreters
* XState JSON compatibility
* Pythonic API (class, builder, functional)
* Hierarchical & parallel states
* Guards, actions, services, delayed transitions
* Actor model (spawn child machines)
* Plugin system & LoggingInspector
* Snapshot save/restore
* Diagram export (Mermaid, PlantUML)
* CLI code generator (5 templates)
* Zero external dependencies
Documentation: https://basiltt.github.io/xstate-statemachine/
PyPI: https://pypi.org/project/xstate-statemachine/
GitHub: https://github.com/basiltt/xstate-statemachine
β 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
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.7.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.