Run your XState or Stately JSON unmodified, or define machines in plain Python.
Async and sync engines share one algorithm. A terminal toolkit that generates, inspects
and simulates machines — and proves its generated code before it writes a file. Zero dependencies.
actionErrorPolicy: "rollback" restores state and context. strict refuses an undeclared event at the call site; strict_config refuses a misspelled key anywhere in the config. A runaway self-send chain is cut and stays on the record. Every policy, with a runnable example →
Time you control
Inject a Clock. SimulatedClock fires a 30‑second timeout in microseconds.
Ask, don't poll
await send("SUBMIT", wait=True) hands you a Receipt the moment the step has run. send_priority jumps the queue; the inbox is bounded with backpressure.
Built-in MemoryStore / FileStore / SQLiteStore (Redis as an extra) behind one persisted() loop: optimistic or fenced locks so two workers never lose an update, an idempotency inbox that answers a replayed webhook with the original receipt, durable after timers a scanner wakes after a restart. What is exactly-once and what is not is written down on the Guarantees page.
[fastapi], [starlette], [litestar], [flask] (with its Quart shim), [django], [channels] and [drf] turn a chart into routes: one POST per event (OpenAPI from your Pydantic events on FastAPI and Litestar, drf-spectacular on DRF), receipts mapped to 200/409/422, SSE, WebSocket and Django Channels streams, a Django admin with CSRF-protected transition buttons, [sqlalchemy] columns and stores. authorize= is required, error bodies never leak exception text, and the core stays zero-dependency.
CloudEvents envelopes, a transactional outbox that commits with the snapshot, an inbound dispatcher with per-subject order and inbox dedup, dead letters with xsm dlq, sagas and choreography. Adapters for Redis Streams, Kafka, RabbitMQ, NATS and SQS, plus a Celery bridge for invoked tasks, Beat-driven durable timers and worker helpers.
[observability] emits OpenTelemetry spans and Prometheus metrics with label hygiene, structlog/loguru/Sentry hooks; a stdlib live inspector speaks the Stately wire protocol. [testing] ships pytest fixtures, state and transition coverage, Hypothesis strategies and replay() for audit logs.
Typed context
context_type=MyCtx flows a TypedDict to interp.context. A typo'd key is a checker error.
xsm generates typed Python (plus recorded tests, typed context, a plugin skeleton), inspects, simulates, diagrams and documents your machines. An interactive launcher on a terminal, plain text when piped — still zero dependencies.
Docs that run
Every Python block in the guide and the README is executed in CI. Every link is resolved. 78 runnable examples.
LLM agents, governed
The model proposes, the machine decides: per-state tool allow-lists, budgets, timeouts and durable human approval. Agents guide · vs LangGraph
Four ways to define a machine
Pick the style that matches your team. All compile to the same runtime.
{% highlight python %}
from xstate_statemachine import (
State, StateMachine, SyncInterpreter, action, guard
)
class TrafficLight(StateMachine):
machine_id = "trafficLight"
# States
green = State("green", initial=True)
yellow = State("yellow")
red = State("red")
# Transitions
slow_down = green.to(yellow, event="TIMER")
stop = yellow.to(red, event="TIMER")
go = red.to(green, event="TIMER")
@action
def log_change(self, interpreter, context, event, action_def):
print(f"Light: {interpreter.active_state_ids}")
# Run it
machine = TrafficLight.create_machine()
interp = SyncInterpreter(machine).start()
interp.send("TIMER") # green → yellow
interp.send("TIMER") # yellow → red
interp.stop()
{% endhighlight %}
Against the three other Python state-machine libraries in common use — transitions 0.9.3, python-statemachine 3.2.2, sismic 1.6.14, checked against the installed packages on 2026-11-07. A dash means the capability exists in a limited form (a thread-based timeout extension, an optional-dependency diagram, listener callbacks rather than a plugin API).
Same machine shape, each library through its own idiomatic API. Higher is better; bold is fastest in the row (two bold cells: within 3 %, a tie).
Scenario
XState-StateMachine
transitions
python-statemachine
sismic
Flat toggle events/s
95,618
142,079
9,466
13,084
Guard + action events/s
164,764
101,101
8,184
12,595
3-level nested events/s
41,375
8,472
3,842
5,133
Parallel regions events/s
30,972
6,033
3,888
4,750
Delayed transitions timers/s
9,337
70
612
6,119
1,000 instances instances/s
51,135
38,311
3,475
7,956
Construction machines/s
12,273
9,335
1,461
285
Native asyncio events/s
26,614
39,117
6,870
—
transitions is faster on the flattest machine (a two-state toggle, 1.35× ours) and on native asyncio
(1.43×) because it is a transition table, not a statechart engine — no configuration set, no entry/exit
ordering, no internal queue. Add a guard and an action and the row is ours (1.63×, since the sync send()
fast paths); the moment states nest or run in parallel, that algorithm is what the others have to emulate, and
XState-StateMachine is 4.8–5.2× faster than the next library. Construction is
31 % ahead of transitions and fanning out to 1,000 instances is
33 % ahead of transitions — while still running the full build-time validator on
every create_machine(), and 6–43× ahead of the other two on both rows.
Measured 2026-11-07 on 0.12.1 against the latest releases (transitions 0.9.3, python-statemachine 3.2.2, sismic 1.6.14;
three quiet sessions, the one closest to their medians shown), all libraries in one session:
Python 3.14, median of 7 runs, GC disabled, setup excluded. Reproduce with benchmarks/competitors/run.py;
full table and caveats in the
benchmark README,
our own scaling curves in Production Characteristics.
Ship state you can reason about.
Start with the guide, or read the whole API on one page.