Integration extras
Every optional pip extra — pydantic, redis, starlette, fastapi, litestar, flask, sqlalchemy, django, drf, channels, agents, observability, cloudevents, celery, the brokers (redis streams, kafka, rabbitmq, nats, sqs) and testing — with its status, issue and the point at which a missing dependency is reported.
The core library has zero runtime dependencies, and that never changes. Everything that talks to a framework — Django models, FastAPI routers, SQLAlchemy types, Celery tasks, Kafka consumers, OpenTelemetry spans — lives in optional packages under xstate_statemachine.contrib, installed one pip extra at a time:
pip install "xstate-statemachine[fastapi]"
pip install "xstate-statemachine[django,sqlalchemy]"
pip install "xstate-statemachine[all]"
import xstate_statemachine still imports nothing third-party. Importing an integration without its extra fails loudly and helpfully:
from xstate_statemachine import MissingExtraError
from xstate_statemachine.contrib._compat import require_extra
try:
require_extra("fastapi", "a_module_that_is_not_installed")
except MissingExtraError as exc:
print(exc)
# `a_module_that_is_not_installed` is not installed. Install the extra: pip install "xstate-statemachine[fastapi]"
assert isinstance(exc, ImportError) # existing `except ImportError` fallbacks keep working
When the error is raised. Importing an integration subpackage checks its pinned dependencies at import time (
import xstate_statemachine.contrib.fastapi). Three things are checked later, on first use, because they are optional soft dependencies that no extra pins:StructlogPlugin()/LoguruPlugin()(structlog/loguru),SentryPlugin()(sentry-sdk),LangChainCallbackPlugin()(langchain-core— its module still needslanggraphat import, from[agents]), and[testing]’s Hypothesis helpers (model_test,events_strategy,payload_strategy).xstate_statemachine.contrib.brokersitself imports with no extra; each adapter module (brokers.kafka, …) checks its own. The Quart shim (contrib.quart) needspip install quarton top of[flask]— the error says so.
The extras
| Extra | Gives you | Status |
|---|---|---|
pydantic |
Typed context validated on every assign, typed events as discriminated unions, machine-JSON validation, JSON Schema export |
shipped — guide · #266 |
observability |
OpenTelemetryPlugin spans, PrometheusPlugin metrics, StructlogPlugin / LoguruPlugin context binding, SentryPlugin breadcrumbs, instrument_all() — all from the plugin hooks, with an X0.6 label allow-list |
shipped — guide · #273 |
testing |
pytest plugin: xstate_machine marker → xsm_interp / xsm_ainterp / xsm_clock / xsm_ran / xsm_store fixtures, xstate_guards_false, xsm_send_all, file-backed xsm_snapshot assertions; also path generation, state/transition coverage (--xsm-coverage), Hypothesis model-based testing (model_test, events_strategy), given / Scenario and replay assertions |
shipped (plugin) — guide · #268 |
redis |
Shared snapshot store, inbox and log for multi-worker deployments | shipped — guide · #306 |
sqlalchemy |
StatechartType, mixin with optimistic locking, stores for sync and AsyncSession, inbox + log, transactional outbox (SQLAlchemyOutboxStore) |
shipped — guide · #284 |
starlette |
Store-backed registry, Receipt → HTTP mapping, SSE / WebSocket transition streaming |
shipped — guide · #275 |
fastapi |
Depends(get_interpreter), StatechartRouter with OpenAPI generated from the chart |
shipped — guide · #276 |
litestar |
XStatePlugin, Provide() dependency, statechart controller |
shipped — guide · #278 |
flask |
XState extension (init_app), blueprint per machine, session-keyed wizards, flask xsm CLI, Quart shim |
shipped — guide · #285 |
django |
StatechartField (queryable sibling columns, in_state()), model mixin with transaction.atomic() + select_for_update or optimistic send_with_retry, signals, TransitionLog audit in the same transaction (append-only in the admin), PermissionGuard / has_event_permission, a transactional outbox (DjangoOutboxStore + OutboxRelay, at-least-once), admin transition buttons, xsm_* management commands, DjangoStore, xsm_deadlines, xsm_migrate_fsm from django-fsm-2 |
shipped — guide · #280–#282 · #310 |
drf |
StatechartViewSetMixin with an @action per event, receipt → status, Idempotency-Key, drf-spectacular schema, StatechartSerializerField |
shipped — guide · #283 |
channels |
StatechartConsumer: snapshot on connect, every transition committed anywhere (admin, REST, socket) pushed to every connection on the row, auth on connect (close 1008) |
shipped — guide · #283 |
celery |
A Celery task as an invoke service; @statechart_task worker act-loop; Celery Beat as the durable after scheduler |
✅ shipped — Celery (#292) |
cloudevents |
CloudEvents SDK objects and HTTP binary / structured interop for the core Envelope (the envelope, dispatcher, outbox, dead letters, sagas and AsyncAPI are core and need no extra) |
shipped — guide · #293 |
kafka · rabbitmq · nats · sqs |
Broker adapters (plus Redis Streams in [redis]): consume envelopes into machines, publish tagged transitions |
✅ shipped — Brokers (#294) |
agents |
TOOL_LOOP chart, tool registry with per-state allow-lists enforced in run_tool, budgets, timeouts, durable human-in-the-loop, structured output, OpenAI/Anthropic adapters (soft imports), spawn_agent + BudgetPlugin multi-agent recipes |
shipped — guide · #287 · #290 |
web · eda · all |
Umbrella extras: web = fastapi + django + drf + flask + sqlalchemy; eda = celery + redis + kafka + rabbitmq + cloudevents + observability; all = every extra above |
— |
The framework versions each shipped extra is tested against (oldest and newest, one CI cell each) are on the Compatibility page. All contrib APIs are provisional under the Deprecation Policy.
What every integration promises — and does not
Each integration page carries two boxes. They are not boilerplate; they are the contract.
Guarantees. What this does: the specific, testable behaviour (for example: “a transition and its audit row are written in one transaction”). What this does not do: the honest limits (for example: “at-least-once — pair with the idempotency inbox; never exactly-once”).
Threat model. Who can call this surface, what it exposes (state only, never raw context, unless you opt in), and what you must configure (an
authorize=callable is required on every generated HTTP route).
The rules every integration follows are collected in the programme’s security & operability baseline.
Design rules (for contributors)
- A subpackage’s
__init__.pystarts withrequire_extra("<extra>", "<module>"). Core never importscontrib; CI proves it with an import-blocking guard. - The single table of extras is
xstate_statemachine.contrib._registry.EXTRAS;pyproject.toml, the CI matrix and the tests are checked against it. - Every contrib module type-checks with its dependency absent (
TYPE_CHECKINGguards) and installed. - Each extra has its own CI cell that installs only that extra, so an integration can never pass because another one happened to be present.