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], [django] and [drf] turn a chart into routes: one POST per event with OpenAPI from your Pydantic events, 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.
Scenario
XState-StateMachine
transitions
python-statemachine
sismic
Flat toggle events/s
71,210
124,773
7,365
12,153
Guard + action events/s
61,777
105,978
6,410
10,935
3-level nested events/s
28,502
7,806
2,901
4,669
Parallel regions events/s
25,349
5,654
2,537
4,333
Delayed transitions timers/s
7,706
79
2,077
5,037
1,000 instances instances/s
33,213
30,215
2,221
7,109
Construction machines/s
8,684
7,649
854
220
Native asyncio events/s
23,609
30,926
5,355
—
transitions is faster on flat machines because it is a transition table, not a statechart
engine — no configuration set, no entry/exit ordering, no internal queue. The moment states nest or run in
parallel, that algorithm is what the others have to emulate, and XState-StateMachine is 3.7–4.5× faster
than the next library. Construction and fanning out to 1,000 instances are 14 % and 10 % ahead of
transitions (stable across two full runs) — while still running the
full build-time validator on every create_machine(), and 15–39× ahead of the other two.
Measured 2026-10-11 on 0.11.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.