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 onDone transition from compound states is included in the exported diagrams, showing the flow from the compound state to the next top-level state.