v0.1.0 is out — release notes

State machines for Rust, blazing fast.

Run your XState v5 or Stately JSON unmodified, or declare statecharts with declarative Rust macros. Sub-microsecond transitions (106 ns p50) with 0 steady-state heap allocations. Tokio actors, durable persistence, transactional outbox EDA, and Stately Studio visual inspection. 15 modular workspace crates.

$ cargo add xstate-statemachine
  • 106 ns p50 transitions
  • 0 heap allocs
  • 15 workspace crates
  • MIT licensed
order.machine.json live
pending submitted open filled cancelled
state pending event —

Everything a statechart runtime should do.
Nothing it shouldn't.

Sub-Microsecond Zero-Allocation Fast Path

Plain microsteps execute in 106 ns p50 (>9.4M ops/s) with 0 heap allocations. Active states use FixedBitSet; identifiers use stack-inlined SmolStr; transition actions execute inline with SmallVec.

use xstate_statemachine::prelude::*;
let mut door = SyncInterpreter::new(machine, logic);
door.start()?;
let receipt = door.send("OPEN")?; // 106 ns transition
assert_eq!(receipt.state_ids(), ["door.opened"]);

XState v5 & Stately Compatible

Load JSON exported from Stately Studio directly into Rust without modification. 53 conformance fixtures verify 100% parity against Node XState.

Tokio Actors & Mailboxes

Spawn actors as Tokio tasks with bounded mailboxes, backpressure policies (Block, DropNewest, Error), and precision cancel-safe timers.

Virtual Clock Testing

SimulatedClock advances virtual time instantaneously. Test a 72-hour saga or 30-day billing trial deterministically in 2 milliseconds without sleeping.

Ask, Don't Poll

actor.send_and_wait("PAY", Duration::from_secs(5)).await awaits the statechart transition and returns a typed Receipt the moment the microstep completes.

Receipt state_ids=["order.paid"] unhandled=false actions_executed=2 duration=106ns

Durable Persistence

Durable stores for SQLite, PostgreSQL, Redis, and File. Layout v4 binary snapshots with 16-hex character topology hashes compatible with the Python library.

Axum Web & Live Inspector

Expose statecharts as REST APIs, Server-Sent Events (SSE), or WebSockets via Axum. Connect the Stately Studio Inspector live over WebSocket/SSE to visualize in real time.

Transactional Outbox EDA

CloudEvents 1.0 messaging with atomic outbox commits. Native adapters for NATS JetStream, RabbitMQ, AWS SQS, and Redis Streams with dead-letter queue (DLQ) support.

Prometheus Observability

Clean metric emissions via the metrics facade: transition rates, step latencies, queue depths, error counters, and active actor gauges with strict label hygiene.

Proc Macro DSL

#[derive(TypedEvent)], #[derive(TypedState)], and the statechart! DSL give you compile-time type-checked transitions without runtime string typos.

The `xsm` Companion CLI

Run xsm validate on your JSON charts, xsm diagram to export Mermaid/PlantUML, xsm inspect --live to bridge to Stately, and xsm simulate to walk paths.

Resilience & Sagas

Distributed saga orchestrator with automated backward compensation rollbacks. Full-jitter exponential backoff, circuit breakers, and idempotency key caches.

LLM Agent Guardrails

The model proposes actions; the statechart decides what is permitted. Enforce per-state tool allow-lists, token budget guards, and durable human approvals.

Define in JSON, Macros, or Dynamic Code

Pick the style that matches your project. All compile to the same sub-microsecond runtime.

use xstate_statemachine::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Load an XState v5 / Stately JSON chart
    let machine = Machine::from_json(r#"{
        "id": "trafficLight",
        "initial": "green",
        "states": {
            "green":  { "on": { "TIMER": "yellow" } },
            "yellow": { "on": { "TIMER": "red" } },
            "red":    { "on": { "TIMER": "green" } }
        }
    }"#)?;

    // 2. Attach dynamic Rust closures for guards and actions
    let logic = MachineLogic::dynamic()
        .action("logChange", |scope| {
            println!("State changed: {:?}", scope.active_state_ids);
            Ok(())
        });

    // 3. Drive with zero-allocation interpreter (106 ns transitions)
    let mut interp = SyncInterpreter::new(machine, logic);
    interp.start()?;
    interp.send("TIMER")?;
    assert_eq!(interp.active_state_ids(), ["trafficLight.yellow"]);
    Ok(())
}
use xstate_statemachine::prelude::*;
use xstate_statemachine::macros::statechart;

statechart! {
    name: TrafficLight;
    initial: Green;
    states: {
        Green {
            on: {
                Timer => Yellow,
            }
        }
        Yellow {
            on: {
                Timer => Red,
            }
        }
        Red {
            on: {
                Timer => Green,
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut actor = TrafficLight::new();
    actor.start()?;
    actor.send(TrafficLightEvent::Timer)?;
    assert_eq!(actor.state(), TrafficLightState::Yellow);
    Ok(())
}
use xstate_statemachine::prelude::*;
use xstate_statemachine::tokio::{ActorOptions, OverflowPolicy};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let machine = Machine::from_json(include_str!("../tests/fixtures/order.json"))?;
    let logic = MachineLogic::dynamic();

    // Spawn concurrent actor with bounded mailbox and backpressure
    let actor = xstate_statemachine::tokio::spawn(
        machine,
        logic,
        ActorOptions::default()
            .with_capacity(1024)
            .with_overflow(OverflowPolicy::Block),
    ).await?;

    // Send and wait for transition receipt
    let receipt = actor.send_and_wait("SUBMIT", Duration::from_secs(5)).await?;
    assert_eq!(receipt.state_ids(), ["order.submitted"]);

    actor.stop().await?;
    Ok(())
}
use xstate_statemachine::patterns::saga::{Saga, SagaStep, SagaStatus};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let saga = Saga::builder("ecommerce-checkout")
        .step(
            SagaStep::new("reserve_inventory")
                .action(|ctx| async move { inventory::reserve().await })
                .compensation(|ctx| async move { inventory::release().await }),
        )
        .step(
            SagaStep::new("charge_payment")
                .action(|ctx| async move { payment::charge().await })
                .compensation(|ctx| async move { payment::refund().await }),
        )
        .build();

    let outcome = saga.execute(context).await?;
    assert_eq!(outcome.status, SagaStatus::Completed);
    Ok(())
}

Proven Micro-Benchmark Performance

Real Criterion benchmark measurements on identical machines (AMD Ryzen 9 7950X / Apple M3 Max).

Scenario Workload Description Rust (xstate-rs) Python (0.12.1) Speedup Allocations
S1 Plain microstep (no actions, no guards) 106 ns 1,520 ns 14.3× faster 0 allocs
S2 Guarded microstep (boolean predicate) 118 ns 1,640 ns 13.9× faster 0 allocs
S3 Transition with context assignment 185 ns 2,380 ns 12.9× faster 0 allocs
S4 Deep hierarchy transition (5 levels) 310 ns 4,920 ns 15.9× faster 0 allocs
S5 Parallel state entry (3 orthogonal regions) 420 ns 6,810 ns 16.2× faster 0 allocs
S6 History restoration (deep history) 235 ns 3,450 ns 14.7× faster 0 allocs
S7 Event rejection (unhandled event) 48 ns 510 ns 10.6× faster 0 allocs
S8 Full binary snapshot serialize & restore 1,840 ns 28,400 ns 15.4× faster Zero-copy serde

Start Exploring

Dive into the guides, architecture comparisons, and production cookbook.

🚀 Getting Started Guide

Build your first statechart in 60 seconds. Learn the dynamic and macro APIs, actions, guards, and interpreters.

🍲 Cookbook & Gallery

Eight copy-pasteable real-world recipes: distributed sagas, circuit breakers, outbox EDA, durable persistence, Axum HTTP.

⚖️ Competitor Comparisons

Detailed technical analysis vs statig, smlang, Python statemachine, XState v5, and Temporal.

📚 Rust API Reference

Exhaustive rustdoc documentation covering all 15 modular workspace crates, traits, structs, and error types.