xstate-statemachine vs Burr
An honest comparison of xstate-statemachine's [agents] extra and Burr: hierarchy, guards as policy, durable timeouts, human-in-the-loop, persistence, observability, and when to choose which.
Burr (Apache, incubating) models an application as actions and transitions over a state object, with persisters, a tracking UI and OpenTelemetry. It is the closest in spirit: explicit state machines for LLM apps.
This table is generated from docs/_data/comparisons.json, and corrections are welcome as PRs against that file. Every row carries a source note. It was checked against apache-burr 0.43.0 (PyPI; formerly burr, last 0.42.0), 2026-10-10; see Burr for the current state. A test (tests/test_battle_291_a.py) fails the nightly comparisons CI job when this check is more than 180 days old, or when the installed release’s major version differs from the one checked, so the table cannot silently go stale (a pull request is never blocked by a competitor’s release).
Feature table
| Capability | xstate-statemachine | Burr | Source |
|---|---|---|---|
| Hierarchy (nested states) | Yes: compound states at any depth, XState v5 semantics | Flat actions + transitions; sub-applications by composition | apache-burr 0.43.0: burr.core.graph.GraphBuilder; sub-applications via burr.core.parallelism.RunnableGraph |
| Parallel regions | Yes: type: parallel, done.state join |
Parallelism via MapStates / sub-applications |
apache-burr 0.43.0: burr.core.parallelism.MapStates / MapActions |
| Guards as policy | Named guards in the chart; run_tool re-checks per-state tool allow-lists (X0.13) |
Conditions on transitions (when, expr) |
apache-burr 0.43.0: burr.core.when, expr, default |
after timeouts |
Yes: durable deadlines for model, tool and human waits, persisted and woken by DueTimerScanner |
Not built in | apache-burr 0.43.0: no timer / delay API in burr.core |
| Human-in-the-loop as durable state | awaiting_human is a persisted state with an escalation deadline; approval names exact call ids |
Halt before/after an action, resume from persisted state | apache-burr 0.43.0: Application.run(halt_before=, halt_after=) + persisters |
| Persistence / replay | Snapshots in SQLite / Redis / SQLAlchemy (e.g. Postgres) stores, versioned layout, transition-log replay() |
Persisters + tracking UI replay (strong) | apache-burr 0.43.0: burr.core.persistence, burr.integrations.persisters (Postgres, Redis, Mongo, SQLite) |
| Visual editor | Opens in the Stately editor (the chart is XState JSON; tool policy in meta, plus one engine key, actionErrorPolicy) |
Burr UI (tracking, graph view) | apache-burr 0.43.0: burr[tracking] UI |
| Typed context / events | Pydantic context + event schemas ([pydantic]), strict tool-argument schemas |
State object; typed via the Pydantic integration | apache-burr 0.43.0: burr.integrations.pydantic.PydanticTypingSystem |
| Observability | JSONL trace with gen_ai.* fields, OTel spans (on_span="otel"), no content by default, secret-key redaction |
Burr UI + OTel | apache-burr 0.43.0: burr.tracking, burr.integrations.opentelemetry |
| Multi-agent | Reference supervisor / pipeline / debate charts; spawn_agent with per-child and global budgets |
Composition of applications | apache-burr 0.43.0: RunnableGraph / sub-applications |
| Incremental adoption | One LangGraph node (statechart_node), a graph as a service (langgraph_service), a pydantic-ai Tool |
Wraps any Python function | apache-burr 0.43.0: @action decorates any function |
When to choose Burr
- You want the Burr UI for step-by-step tracking and replay out of the box.
- A flat action graph with
whenconditions fits your app, and you do not need nested or parallel states. - You prefer defining the machine with Python decorators over a JSON chart.
- You want Burr’s persisters for Postgres, Redis, MongoDB and SQLite, and
MapStates/MapActionsfor fan-out, without writing a chart.
When to choose xstate-statemachine
- You need hierarchy and parallel regions with standard statechart semantics (XState v5), not only flat transitions.
- You want durable
afterdeadlines: timeouts and escalations that survive restarts. - The chart should be portable XState JSON, editable in Stately and shareable with a TypeScript frontend.
- You want per-state tool allow-lists that
run_toolenforces regardless of the guards (X0.13).
Ours, in 20 lines
This is a support agent. The refund tool is legal only in the tool states and needs a human to approve it, and the whole run is capped at 5 turns and 10 cents. It runs offline with FakeModel:
import asyncio
from xstate_statemachine.contrib.agents import (
FakeModel,
load_chart,
run_agent,
tool,
tool_registry,
)
def lookup(order_id: int) -> str:
"""Look up an order."""
return f"order {order_id}: shipped"
def refund(order_id: int) -> str:
"""Refund an order."""
return "refunded"
chart = load_chart() # TOOL_LOOP, plain XState JSON
for s in ("awaiting_model", "awaiting_tool"):
chart["states"][s]["meta"]["tools"] = [
"lookup",
"refund",
] # policy lives in the chart
tools = tool_registry(
tool(lookup, timeout_s=5), tool(refund, timeout_s=5, side_effect=True)
)
model = FakeModel(
[
{
"tool": "lookup",
"args": {
"order_id": 42,
},
},
{
"tool": "refund",
"args": {
"order_id": 42,
},
},
]
)
res = asyncio.run(
run_agent(
chart,
model=model,
tools=tools,
prompt="refund 42",
budgets={
"max_turns": 5,
"max_usd": 0.10,
},
)
)
assert (
res.waiting and res.final_state == "toolLoop.awaiting_human"
) # durable: persist, resume later
See LLM agents for the full reference, including the LangGraph interop and pydantic-ai adapters.
New to the library? Start with the integrations journey: pick your path, then a fifteen-minute tutorial.