EntitySession¶
EntitySession is a stateful wrapper around a StateMachine. It tracks the current state and context for one entity (one order, one user, one workflow instance) and provides a developer-friendly API on top of the raw FSM.
Creating a session¶
from pystator import StateMachine
machine = StateMachine.from_yaml("order_fsm.yaml")
# Create an instance for entity "order-123"
order = machine.create(context={"order_id": "order-123", "customer_id": "cust-456"})
machine.create() is a convenience wrapper — you can also instantiate directly:
from pystator import EntitySession
order = EntitySession(machine, context={"order_id": "order-123"})
Sending events¶
Synchronous¶
result = order.send("confirm")
print(result.success) # True
print(result.source_state) # "pending"
print(result.target_state) # "confirmed"
print(result.trigger) # "confirm"
print(result.error) # None (or exception message on failure)
Pass payload data alongside the trigger:
The payload is merged into the context and available to guards and actions.
Auto-generated trigger methods¶
For each trigger defined in the YAML, EntitySession automatically creates a method on the instance:
order.confirm() # same as order.send("confirm")
order.ship(tracking="TRK") # same as order.send("ship", tracking="TRK")
order.cancel(reason="oos") # same as order.send("cancel", reason="oos")
Trigger names in YAML must already be valid identifiers (lowercase a–z, digits, underscores). They are converted to Python method names by normalizing to snake_case (e.g. submit_order stays submit_order). Dots are not allowed in YAML trigger names.
Async¶
All async guards require asend:
State queries¶
Auto-generated is_<state> properties¶
For each state in the YAML, EntitySession auto-generates a boolean property:
order.is_pending # True (state names become is_<snake_case>)
order.is_confirmed # False
order.is_final # False (not a terminal state)
Other properties¶
order.current_state # "pending"
order.allowed_triggers # ["confirm", "cancel"]
order.history # list of TransitionResult from all sends
order.context # current context dict (mutable)
Validate a target state (no trigger name)¶
When integration data only carries a status (state name), use evaluate_destination to see if that target is allowed from current_state with the session context. Optional context_overrides are merged into context for the check only (the session dict is not mutated).
check = order.evaluate_destination("confirmed")
check.allowed # True if some transition can reach "confirmed" now
check = order.evaluate_destination("filled", context_overrides={"fill_qty": 100})
order.can_transition_to("filled") # bool; same as check.allowed
If multiple triggers lead to the same destination, check.is_ambiguous (and check.metadata.get("ambiguous")) is True when more than one path succeeds—pick a policy in your app if you need a single trigger.
Snapshots — persist and restore¶
snapshot() serializes the entire session state into a plain dict you can store in a database, cache, or message queue. Restore it with from_snapshot():
# Persist
snap = order.snapshot()
# {
# "current_state": "confirmed",
# "context": {"order_id": "order-123", ...},
# "machine_name": "order_lifecycle",
# "machine_version": "1.0.0",
# }
redis_client.set("order:order-123", json.dumps(snap))
# Restore later
data = json.loads(redis_client.get("order:order-123"))
order = EntitySession.from_snapshot(machine, data)
print(order.current_state) # "confirmed"
The snapshot is JSON-serializable (assuming your context values are serializable).
Deferred events¶
Set defer_unhandled=True when creating a session to queue events that don't match any transition in the current state, and replay them automatically after the next successful state change:
order = EntitySession(machine, context={}, defer_unhandled=True)
order.send("confirm") # no transition for "confirm" in state "pending" → deferred
order.send("open") # transitions to "open", then replays "confirm" → transitions to "confirmed"
When to use it: Useful for event-sourced systems where events may arrive out of order (e.g., a "payment_received" event arrives before the "order_placed" transition has been applied).
Inspecting deferred events:
print(order.deferred_events) # list of (trigger, payload) tuples
order.clear_deferred() # discard all queued events
EventRecorder — record and replay¶
EventRecorder wraps an EntitySession and records every event sent through it. The recording can then be replayed on a fresh session (e.g., to replay a sequence in tests or for debugging):
order = machine.create()
recorder = order.record() # wrap with recorder
recorder.send("confirm")
recorder.send("ship", tracking="TRK-001")
recorder.send("deliver")
print(recorder.events)
# [RecordedEvent(trigger='confirm', payload={}, timestamp=...), ...]
# Replay on a fresh instance
fresh = machine.create()
result = recorder.replay(fresh) # applies all recorded events
print(fresh.current_state) # "delivered"
Saving and loading recordings¶
import json
# Save
recording_data = [e.to_dict() for e in recorder.events]
Path("recordings/order-123.json").write_text(json.dumps(recording_data))
# Load and replay
from pystator.instance import RecordedEvent
raw = json.loads(Path("recordings/order-123.json").read_text())
events = [RecordedEvent.from_dict(e) for e in raw]
fresh = machine.create()
for ev in events:
fresh.send(ev.trigger, **ev.payload)
Thread safety¶
EntitySession.send() is thread-safe — it uses an internal threading.Lock. Do not share a single EntitySession across threads and async code; use either send (threads) or asend (async) exclusively within a given instance.
Example: order lifecycle¶
from pystator import StateMachine
machine = StateMachine.from_yaml("order_fsm.yaml")
order = machine.create(context={"order_id": "ORD-1"})
print(order.current_state) # "pending"
print(order.is_pending) # True
order.confirm()
print(order.current_state) # "confirmed"
result = order.ship(carrier="FedEx", tracking="1234")
print(result.success) # True
print(order.current_state) # "shipped"
snap = order.snapshot()
# ... persist snap ...
restored = machine.create_from_snapshot(snap)
print(restored.current_state) # "shipped"
print(restored.is_final) # False
See also¶
- Concepts guide — entity, session, context explained
- Orchestrator — multi-entity processing with persistence
- State Stores — persist snapshots across restarts
- API Reference — EntitySession