Guide¶
Config shape¶
StateMachine.from_dict(config, action_dict=None, guard_dict=None) takes a plain dict mapping
state name → state config. Each state config accepts:
Field |
Meaning |
|---|---|
|
|
|
Eventless transition candidates, re-checked after every state entry. |
|
Action names to run when the state is entered. |
|
Action names to run when the state is left. |
|
Fallback state to enter if an |
A transition candidate is {"target": ..., "guard": ... (optional), "actions": [...] (optional)}.
Shorthand is normalized automatically:
"on": {"START": "running"} # -> target only
"on": {"START": {"target": "running", "guard": "can_start"}} # -> single dict
"on": {"START": ["blocked", {"target": "running", "guard": "g"}]} # -> list of candidates
"*" is a wildcard event — used when no exact event match exists for the current state.
Config is validated eagerly: unknown transition targets (in on, always, or error_state)
raise at construction time, and if action_dict/guard_dict are supplied, every referenced
guard/action name must be registered too.
Guards and actions¶
Both are plain Python callables — sync or async — taking (ctx, signal):
def can_start(ctx, signal) -> bool: ... # guards must return bool
async def create_order(ctx, signal) -> None: ... # actions may return anything (or nothing)
Register them via from_dict(config, action_dict={...}, guard_dict={...}), or later through
machine.actions.register(name, fn) / machine.guards.register(name, fn), followed by
machine.validate_registries() to check for typos (raises ValueError listing anything missing).
Running the machine¶
state = await machine.run(run_id=None, state_name=state_name, events=events, session=session)
All arguments are keyword-only.
run_id— an optional correlation id used in log lines (run_id=... | state=... | ...). If not provided (left asNone), auuid4().hexstring is generated automatically.events— a singleSignal, a list ofSignals, or[]to only resolve pendingalways-transitions for the current state.session— any object you want. The engine never inspects or mutates its shape; it’s simply attached to the per-runContext(ctx.session) so your guards/actions can read and mutate whatever they need.Returns the final state name once every signal has been processed and all pending
always-transitions have settled.
always-transitions¶
After every state entry (including the initial state, before any signal is processed), the
engine checks that state’s always list. If a guard passes, its actions run, the machine enters
the target, and the check repeats from there — so one run() call can auto-advance through
several states in a row. If no guard passes, nothing happens. This loop is capped at 100 hops; an
always chain that never settles raises RuntimeError.
error_state fallback¶
If an on-transition’s action raises, the engine wraps it as TransitionError. If the current
state declares error_state, the machine transitions there instead of raising (a warning is
logged); otherwise the TransitionError propagates to the caller.
Tracing a run¶
Every guard/action evaluated during a run() call is recorded, in order, on
ctx.results (a list of ResultEntry). show_transitions(ctx)
renders that trace as a readable hop-by-hop report — useful for debugging why a machine ended up
where it did. Note run() itself only returns the final state name; to inspect the trace, work
with a Context directly (as the test suite does) or rely on the run_id-tagged log
output.
Visualizing the graph¶
to_mermaid(machine, initial=None) walks machine.config and returns
a Mermaid stateDiagram-v2 diagram source string — pure text, no extra dependency. Paste the
result into a ```mermaid fence and it renders natively in GitHub READMEs, Sphinx
(including this site, via sphinxcontrib-mermaid), VS Code, and Jupyter.
from statem import StateMachine, to_mermaid
config = {
"idle": {"on": {"START": {"target": "running", "guard": "can_start"}}},
"running": {"on": {"STOP": "idle"}},
}
machine = StateMachine.from_dict(config, guard_dict={"can_start": lambda ctx, signal: True})
print(to_mermaid(machine, initial="idle"))
Output:
stateDiagram-v2
[*] --> idle
idle --> running: START [can_start]
running --> idle: STOP
Which renders as:
stateDiagram-v2
[*] --> idle
idle --> running: START [can_start]
running --> idle: STOP
A guard-chain (multiple candidates for one event) produces multiple labeled edges from the same
state, and always/error_state get their own always/error-labeled edges — see the
bank teller bot for a richer diagram that shows all
of this at once. initial, if given and present in config, prepends a [*] --> initial edge;
StateMachine itself has no notion of an initial state (that’s chosen fresh by the caller on
every run() call), so it’s opt-in.
Observing a run as it happens¶
run() returns only once everything has settled. If a caller needs to see each hop as it
happens — e.g. to stream progress to a UI — use stream() instead: same engine,
same effect on session, but yields AG-UI protocol events (STEP_STARTED, STATE_SNAPSHOT,
ACTIVITY_SNAPSHOT, STATE_DELTA, STEP_FINISHED) as it goes. See the
Streaming page for the full event contract.
Further reading¶
The API Reference documents every public class and function directly from source.