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 this site is executed in CI. Every link is resolved. 34 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.1, sismic 1.6.11, checked against the installed packages on 2026-09-19. 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
80,960
139,176
8,251
11,916
Guard + action events/s
94,903
83,360
8,053
11,810
3-level nested events/s
35,252
6,701
3,487
4,784
Parallel regions events/s
26,411
5,707
3,291
4,392
Delayed transitions timers/s
8,472
66
2,565
5,446
1,000 instances instances/s
45,640
34,974
2,680
6,294
Construction machines/s
12,659
8,716
1,226
250
Native asyncio events/s
24,836
35,157
6,355
—
transitions is faster on the flattest machine (a two-state toggle) and on native asyncio 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 rows level out (1.14× ours); the moment states nest or run in
parallel, that algorithm is what the others have to emulate, and XState-StateMachine is 4.6–5.3× faster
than the next library. Construction is 45 % ahead of transitions and
fanning out to 1,000 instances is 30 % ahead of transitions — while still running the
full build-time validator on every create_machine(), and 7–51× ahead of the other two
on both rows. Measured 2026-11-05 on 0.12.0, 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.