Diagram Export
Generate PlantUML and Mermaid diagrams from any machine definition.
Diagram Export
Every MachineNode can export its structure as a PlantUML or Mermaid diagram. This lets you visualize your state machines, generate documentation, and keep diagrams in sync with your code automatically.
🖼️ Overview
flowchart LR
M["🧠 machine<br/><small>JSON or Pythonic</small>"] --> A["to_mermaid()"] --> R1["GitHub · docs · Notion render it live"]
M --> B["to_plantuml()"] --> R2["PlantUML server · IDE plugins → PNG / SVG"]
The library provides two export methods on every MachineNode:
| Method | Format | Use Case |
|---|---|---|
machine.to_plantuml() |
PlantUML | Rich diagrams, IDE plugins, PDF generation |
machine.to_mermaid() |
Mermaid | GitHub README, Markdown docs, online editors |
Both methods return a string that you can print, save to a file, or embed in documentation.
🌿 PlantUML Export
Basic Example
from xstate_statemachine import create_machine
config = {
"id": "trafficLight",
"initial": "red",
"states": {
"red": {"on": {"TIMER": "green"}},
"green": {"on": {"TIMER": "yellow"}},
"yellow": {"on": {"TIMER": "red"}}
}
}
machine = create_machine(config)
plantuml = machine.to_plantuml()
print(plantuml)
Sample Output
@startuml
hide empty description
state "trafficLight" as trafficLight {
[*] --> trafficLight_red
state "red" as trafficLight_red
state "green" as trafficLight_green
state "yellow" as trafficLight_yellow
}
[*] --> trafficLight_red
trafficLight_red --> trafficLight_green : TIMER
trafficLight_green --> trafficLight_yellow : TIMER
trafficLight_yellow --> trafficLight_red : TIMER
@enduml
How to Render PlantUML
There are several ways to render PlantUML diagrams:
1. Save to file and use the PlantUML CLI:
pip install plantuml
# Save the diagram
with open("traffic_light.puml", "w") as f:
f.write(machine.to_plantuml())
# Render to PNG
plantuml traffic_light.puml
2. Use the PlantUML online server:
Paste the output at https://www.plantuml.com/plantuml/uml/
3. VS Code extension:
Install the “PlantUML” extension and preview .puml files directly in the editor.
🧜 Mermaid Export
Basic Example
from xstate_statemachine import create_machine
config = {
"id": "trafficLight",
"initial": "red",
"states": {
"red": {"on": {"TIMER": "green"}},
"green": {"on": {"TIMER": "yellow"}},
"yellow": {"on": {"TIMER": "red"}}
}
}
machine = create_machine(config)
mermaid = machine.to_mermaid()
print(mermaid)
Sample Output
stateDiagram-v2
[*] --> red
state "trafficLight" as trafficLight {
[*] --> red
}
red --> green : TIMER
green --> yellow : TIMER
yellow --> red : TIMER
The top-level machine is always wrapped in its own state "<id>" as <id> { [*] --> ... } block (the same is true for any nested compound state), alongside a leading [*] --> red line for the overall diagram.
Embedding in GitHub README
Mermaid diagrams render natively in GitHub Markdown. Wrap the output in a fenced code block:
```mermaid
stateDiagram-v2
[*] --> red
state "trafficLight" as trafficLight {
[*] --> red
}
red --> green : TIMER
green --> yellow : TIMER
yellow --> red : TIMER
```
Tip: You can automate this by writing a script that regenerates your README’s diagram section from your machine definition. This ensures your documentation always matches your code.
📄 Export from JSON Machines
Both export methods work with any machine created via create_machine():
import json
from xstate_statemachine import create_machine
# Load config from a JSON file
with open("my_machine.json") as f:
config = json.load(f)
machine = create_machine(config)
# Export both formats
with open("docs/machine.puml", "w") as f:
f.write(machine.to_plantuml())
with open("docs/machine.mmd", "w") as f:
f.write(machine.to_mermaid())
print("Diagrams exported!")
🐍 Export from Pythonic API Machines
Machines built with the Pythonic API export diagrams the same way — StateMachine.create_machine(), build_machine(), and MachineBuilder.build() all return a MachineNode:
from xstate_statemachine import State, StateMachine
class DoorLock(StateMachine):
machine_id = "doorLock"
locked = State("locked", initial=True)
unlocked = State("unlocked")
unlock = locked.to(unlocked, event="UNLOCK")
lock = unlocked.to(locked, event="LOCK")
machine = DoorLock.create_machine()
# PlantUML
print(machine.to_plantuml())
# Mermaid
print(machine.to_mermaid())
Mermaid output:
stateDiagram-v2
[*] --> locked
state "doorLock" as doorLock {
[*] --> locked
}
locked --> unlocked : UNLOCK
unlocked --> locked : LOCK
The functional build_machine() and fluent MachineBuilder produce the same kind of MachineNode:
from xstate_statemachine import State, build_machine
idle = State("idle", initial=True)
running = State("running")
machine = build_machine(
id="switch",
states=[idle, running],
transitions=[idle.to(running, event="START")],
)
print(machine.to_mermaid())
from xstate_statemachine import MachineBuilder
machine = (
MachineBuilder("switch")
.state("idle", initial=True)
.state("running")
.transition("idle", "START", "running")
.build()
)
print(machine.to_mermaid())
Both produce:
stateDiagram-v2
[*] --> idle
state "switch" as switch {
[*] --> idle
}
idle --> running : START
🪆 Nested State Diagrams
Both exporters handle hierarchical (compound) states with proper nesting:
from xstate_statemachine import create_machine
config = {
"id": "editor",
"initial": "idle",
"states": {
"idle": {"on": {"OPEN": "editing"}},
"editing": {
"initial": "drafting",
"states": {
"drafting": {
"on": {
"PREVIEW": "previewing",
"SAVE": "saving"
}
},
"previewing": {"on": {"EDIT": "drafting"}},
"saving": {
"on": {"SAVE_DONE": "drafting"}
}
},
"on": {"CLOSE": "idle"}
}
}
}
machine = create_machine(config)
print(machine.to_plantuml())
PlantUML output (nested):
@startuml
hide empty description
state "editor" as editor {
[*] --> editor_idle
state "idle" as editor_idle
state "editing" as editor_editing {
[*] --> editor_editing_drafting
state "drafting" as editor_editing_drafting
state "previewing" as editor_editing_previewing
state "saving" as editor_editing_saving
}
}
[*] --> editor_idle
editor_idle --> editor_editing : OPEN
editor_editing_drafting --> editor_editing_previewing : PREVIEW
editor_editing_drafting --> editor_editing_saving : SAVE
editor_editing_previewing --> editor_editing_drafting : EDIT
editor_editing_saving --> editor_editing_drafting : SAVE_DONE
editor_editing --> editor_idle : CLOSE
@enduml
🎛️ Parallel State Diagrams
Parallel states are also exported, but both exporters render each parallel region as an ordinary nested composite state — there is no parallel-specific visual marker (no Mermaid -- region divider, no distinct styling in PlantUML). The nesting itself is what shows you the regions:
from xstate_statemachine import create_machine
config = {
"id": "player",
"initial": "playing",
"states": {
"playing": {
"type": "parallel",
"states": {
"video": {
"initial": "loading",
"states": {
"loading": {"on": {"VIDEO_READY": "streaming"}},
"streaming": {"on": {"BUFFER": "loading"}}
}
},
"audio": {
"initial": "muted",
"states": {
"muted": {"on": {"UNMUTE": "audible"}},
"audible": {"on": {"MUTE": "muted"}}
}
}
},
"on": {"STOP": "stopped"}
},
"stopped": {"type": "final"}
}
}
machine = create_machine(config)
print(machine.to_mermaid())
Mermaid output:
stateDiagram-v2
[*] --> playing
state "player" as player {
[*] --> playing
state "playing" as playing {
state "video" as video {
[*] --> loading
}
state "audio" as audio {
[*] --> muted
}
}
}
playing --> stopped : STOP
loading --> streaming : VIDEO_READY
streaming --> loading : BUFFER
muted --> audible : UNMUTE
audible --> muted : MUTE
📚 Using Diagrams for Documentation
Auto-Generate Documentation Script
Create a script that generates diagrams for all your machines:
"""generate_docs.py — Export diagrams for all state machines."""
import json
from pathlib import Path
from xstate_statemachine import create_machine
MACHINES_DIR = Path("configs")
DOCS_DIR = Path("docs/diagrams")
DOCS_DIR.mkdir(parents=True, exist_ok=True)
for config_file in MACHINES_DIR.glob("*.json"):
with open(config_file) as f:
config = json.load(f)
machine = create_machine(config)
machine_id = config["id"]
# Export PlantUML
puml_path = DOCS_DIR / f"{machine_id}.puml"
puml_path.write_text(machine.to_plantuml())
# Export Mermaid
mmd_path = DOCS_DIR / f"{machine_id}.mmd"
mmd_path.write_text(machine.to_mermaid())
print(f"Exported: {machine_id} -> {puml_path}, {mmd_path}")
print(f"\nAll diagrams written to {DOCS_DIR}/")
python generate_docs.py
Complete Example: Complex Machine with All Features
from pathlib import Path
from xstate_statemachine import MachineLogic, create_machine
config = {
"id": "orderSystem",
"initial": "browsing",
"context": {"cart": [], "total": 0},
"states": {
"browsing": {
"on": {
"ADD_ITEM": {"actions": "addToCart"},
"CHECKOUT": {"target": "checkout", "guard": "cartNotEmpty"}
}
},
"checkout": {
"initial": "shipping",
"states": {
"shipping": {
"on": {"SUBMIT_ADDRESS": "payment"}
},
"payment": {
"on": {"SUBMIT_PAYMENT": "processing"}
},
"processing": {
"invoke": {
"src": "processPayment",
"onDone": "confirmation",
"onError": "payment"
}
},
"confirmation": {"type": "final"}
},
"on": {"CANCEL": "browsing"},
"onDone": "complete"
},
"complete": {"type": "final"}
}
}
# Diagrams need only the SHAPE; stub the named logic so the build-time
# check for missing implementations passes.
machine = create_machine(
config,
logic=MachineLogic(
actions={"addToCart": lambda *a: None},
guards={"cartNotEmpty": lambda c, e: True},
services={"processPayment": lambda *a: None},
),
)
# Save both diagram formats
Path("order_system.puml").write_text(machine.to_plantuml())
Path("order_system.mmd").write_text(machine.to_mermaid())
# Print Mermaid for quick preview
print(machine.to_mermaid())
Note: The
onDonetransition from compound states is included in the exported diagrams, showing the flow from the compound state to the next top-level state.