FSM configuration reference¶
This page documents the YAML/JSON structure for PyStator state machine definitions. All keys, types, and validation rules are described so you can author or validate configs without reading the source.
Top-level structure¶
A valid FSM config has these top-level keys:
| Key | Required | Type | Description |
|---|---|---|---|
meta |
No | object | Machine metadata (name, version, strict mode, validate_context, etc.). |
states |
Yes | list | List of state definitions. At least one required. |
transitions |
Yes | list | List of transition definitions. |
groups |
No | object | Named groups for organizing states. Keys are group names; values are objects with an optional description. |
error_policy |
No | object | Error handling (fallback state, retry). |
state_variables |
No | list | State variable definitions (context contract). See below. |
events |
No | list | Documentation-only: described events. |
Example minimal config:
meta:
machine_name: my_fsm
version: "1.0.0"
strict_mode: true
states:
- name: a
type: initial
- name: b
type: stable
- name: c
type: terminal
transitions:
- trigger: go
source: a
dest: b
- trigger: done
source: b
dest: c
Naming rules¶
The schema (Pydantic) validates identifiers for states, regions, triggers, guards, and actions:
| Item | Pattern (regex) | Notes |
|---|---|---|
State name |
^[a-z0-9_]+$ |
Lowercase only; no dots—use parent / initial_child, not dotted paths. |
Region name |
^[a-z0-9_]+$ |
Must be unique across the entire machine if you use region: on transitions (see semantic validation). |
trigger |
^[a-z0-9_]+$ |
Same as state names when present. |
| Guard/action names (string form) | ^[a-z0-9_]+$ |
Declarative guards/effects use other keys (expr, check, set, …). |
meta.machine_name and other meta fields may use different conventions in the UI; the table above applies to states, regions, and transition triggers in the config file.
Triggers sent at runtime (send, process, API) must match YAML spelling (unless you set meta.event_normalizer).
Authoring hierarchy vs parallel states¶
These are different concepts; both appear as a flat states list in YAML:
-
Hierarchical (compound) states: A state may set
parent: <compound_state_name>so it is visually and logically nested under that parent. The parent typically setsinitial_child: <child_name>so entering the parent resolves to that child. Siblings under the same parent are mutually exclusive (OR): only one active child at a time under that compound. -
Parallel states: A state has
type: paralleland aregionslist. Each region has its owninitialandstateslist. All regions are active at once (AND). Useregion: <region_name>on a transition to scope it to one orthogonal region when the engine matches transitions inside a parallel composite. -
Region names: Define each region name once per machine across all parallel states. Duplicate region names on different parallel parents make
region:on transitions ambiguous; the semantic validator rejects that.
Prefer either listing a child in regions[].states or giving it parent: <parallel_state> consistently so validation can confirm membership.
meta¶
Metadata for the machine. All fields are optional but recommended for clarity and for the API/UI.
| Field | Type | Default | Description |
|---|---|---|---|
machine_name |
string | null |
Human-readable name (e.g. for API/UI). |
version |
string | null |
Version string (e.g. "1.0.0"). |
strict_mode |
boolean | true |
If true, semantic validation is enforced (exactly one initial state, no transitions from terminal, etc.). |
event_normalizer |
"lower" | "upper" |
null |
Normalize trigger names to lower or upper case. |
description |
string | null |
Short description of the machine. |
validate_context |
boolean | false |
When true, the engine (and Orchestrator) validate context against state_variables before each transition: required keys must be present; optional type checks (string, number, boolean, object) apply. On failure, transition returns a failed result with validation errors. |
Extra keys in meta are allowed (e.g. for tooling) and ignored by the engine.
groups¶
Optional. Organizes states into named groups for visualization, documentation, and tooling. Groups are metadata only — the engine does not use them for execution logic.
Each key is a group name; the value is an object with an optional description.
groups:
data:
description: "Data collection and optimization"
validation:
description: "Validation and approval gate"
execution:
description: "Order execution and settlement"
terminal:
description: "Cycle outcomes"
Assign a state to a group using the group field on the state definition (see States → optional fields).
states¶
Each item is a state definition object.
State: required fields¶
| Field | Type | Description |
|---|---|---|
name |
string | Unique state identifier. Must match ^[a-z0-9_]+$. |
type |
string | One of: initial, stable, terminal, error, parallel, choice. Default: stable. |
State: optional fields¶
| Field | Type | Description |
|---|---|---|
description |
string | Human-readable description. |
group |
string | Group name from the top-level groups section. Organizational metadata; ignored by the engine. |
parent |
string | Name of parent state for hierarchical (compound) states. |
initial_child |
string | For compound states: the child state to enter when this state is entered. |
regions |
list | For type: parallel only. List of region definitions (see below). |
on_enter |
string | list | Actions to run when entering this state. Supports named actions, parameterized specs, and declarative effects (see Declarative effects). |
on_exit |
string | list | Actions to run when leaving this state. Same format as on_enter. |
timeout |
object | Auto-transition after a duration. Requires seconds (float > 0) and destination (state name). |
invoke |
list | Invoked service refs (advanced; see source for InvokeItemDef). |
metadata |
object | Arbitrary key-value data (ignored by engine). |
State metadata (reference)¶
The engine ignores metadata; it is for documentation and tooling. A common convention is metadata.context_keys: a list of context variable names that apply to or are expected for this state (e.g. ["flight_id", "departure_time", "origin"]). This documents the "state variables" or context contract per state; the engine does not validate or use it.
- name: scheduled
type: initial
metadata:
context_keys: [flight_id, departure_time, origin, crew_assigned]
State types¶
initial— Entry point. Exactly one initial state is required whenstrict_modeis true. The machine starts in this state.stable— Normal state; can have incoming and outgoing transitions.terminal— End state. No outgoing transitions are allowed from a terminal state.error— Used witherror_policy.default_fallbackfor unhandled errors.parallel— Container for orthogonal regions. Must defineregions; each region has its own initial and states.
on_enter / on_exit format¶
Each can be:
- A single action name:
"notify_open" - A list of action names:
["notify_open", "log_audit"] - A list mixing names, parameterized specs, and declarative effects:
on_enter:
- fetch_market_data # named action (Python)
- { name: send_alert, params: { level: info } } # parameterized action
- { set: { phase: "collecting" } } # declarative effect: set fields
- { timestamp: collection_started_at } # declarative effect: record time
Declarative effects are executed inline by the engine with no Python function required. See Declarative effects for the full reference.
Actions are executed by your code via ActionExecutor after you persist the state change.
timeout format¶
seconds— Float, must be > 0. After this many seconds the machine can transition todestination(typically used with a timeout manager or scheduler).destination— State name. Must be a defined state and satisfy naming rules (lowercase identifiers).
Parallel state regions¶
For type: parallel, each region is an object:
| Field | Type | Description |
|---|---|---|
name |
string | Region identifier; used in transitions as region: name. Pattern: ^[a-z0-9_]+$. Must be unique machine-wide among all parallel states. |
initial |
string | Initial state name for this region. |
states |
list of strings | State names that belong to this region (can also be defined as separate state entries with parent). |
description |
string | Optional description. |
Example:
- name: active
type: parallel
regions:
- name: trading
initial: scanning
states: [scanning, analyzing, executing]
- name: risk_monitor
initial: normal
states: [normal, elevated, critical]
transitions¶
Each transition defines: from which state(s), on which event (or after a delay), to which state; optionally with guards and actions.
Transition: required fields¶
| Field | Type | Description |
|---|---|---|
source |
string | list of strings | Source state name(s). If a list, the transition is valid from any of these states. Use "*" to match all non-terminal states. |
dest |
string | Target state name. |
Either trigger or after must be present (or both in some usages):
| Field | Type | Description |
|---|---|---|
trigger |
string | Event name. Your code calls process(current_state, trigger, context). |
after |
integer | string | Delayed transition. Integer = milliseconds. String: e.g. "5s", "10m", "1h". Requires a scheduler. |
Transition: optional fields¶
| Field | Type | Description |
|---|---|---|
guards |
list | Guard names, inline check specs, or inline expressions. All must be true for the transition to fire. See Guard format. |
actions |
list | Action names, parameterized specs, or declarative effects to run after the transition. See Action format. |
region |
string | For parallel states: restrict this transition to this region. |
description |
string | Human-readable description. |
ref |
string | Single spec/requirement reference ID for traceability (e.g. "REQ-42"). |
refs |
list of strings | Multiple spec/requirement reference IDs. Combined with ref if both are present. |
metadata |
object | Arbitrary data (ignored by engine). |
Guard format in YAML¶
A guard list entry can be one of three forms:
Named guard — implemented in Python, registered with GuardRegistry:
Inline expression — evaluated against context variables; requires pystator[api]:
Declarative check — structured field comparison evaluated by the engine with no Python required:
All guards in the list must pass for the transition to fire.
See Declarative checks for all operators and examples.
Action format in YAML¶
An action list entry can be one of three forms:
Named action — implemented in Python, registered with ActionRegistry:
Parameterized action — named action with extra params passed via context:
Declarative effect — inline context mutation evaluated by the engine with no Python required:
See Declarative effects for all effect types and examples.
Validation rules for transitions¶
- Every state name in
sourceanddestmust be defined instates. - No transition may have a terminal state in
source. - If
regionis set, it must name a region defined on exactly one parallel state, and each ordinarysourcestate must belong to that region (see semantic validation). - For delayed transitions (
after), a scheduler must be configured at runtime (e.g.OrchestratorwithAsyncioScheduler,RedisScheduler, orCeleryScheduler).
Declarative effects¶
Declarative effects let you mutate context fields inline in YAML — inside on_enter, on_exit, or actions — without writing a Python function. The engine executes them directly.
Six effect types are available:
set — assign one or more fields¶
Calls ctx.update(...). Accepts a mapping of one or more key-value pairs.
timestamp — record current UTC time¶
Sets ctx["collection_started_at"] to an ISO 8601 UTC string (e.g. "2024-01-15T09:30:00.123456+00:00").
increment — add 1 to a numeric field¶
If the field is absent it starts from 0, so the result is 1. Equivalent to ctx[field] = ctx.get(field, 0) + 1.
decrement — subtract 1 from a numeric field¶
If the field is absent it starts from 0, so the result is -1. Equivalent to ctx[field] = ctx.get(field, 0) - 1.
append — add a value to a list field¶
If the field is absent a new list is created. Equivalent to ctx.setdefault(field, []).append(value).
clear — remove a field from context¶
Silently does nothing if the field is not present. Equivalent to ctx.pop(field, None).
Combining effects with named actions¶
Effects and named actions can be freely mixed in the same list:
on_enter:
- submit_rebalance_orders # named Python action
- { set: { phase: "executing" } } # declarative effect
- { timestamp: execution_started_at } # declarative effect
Declarative checks¶
Declarative checks let you express simple field comparisons as guards inline in YAML — without writing or registering a Python guard function.
Syntax:
For membership operators (in, not_in), use values (a list) instead of value:
Operators¶
| Operator | Description | Requires |
|---|---|---|
eq |
Field equals value | value |
neq |
Field does not equal value | value |
gt |
Field is greater than value | value |
gte |
Field is greater than or equal to value | value |
lt |
Field is less than value | value |
lte |
Field is less than or equal to value | value |
in |
Field is a member of values list | values |
not_in |
Field is not a member of values list | values |
is_set |
Field is present and not null | — |
is_null |
Field is absent or null | — |
Missing fields are treated as null / None for all operators. For ordering operators (gt, gte, lt, lte), a missing/null field returns false.
Examples¶
# Retry guard: allow up to 3 retries
guards:
- { check: { field: retry_count, op: lt, value: 3 } }
# Status must be one of a set
guards:
- { check: { field: status, op: in, values: [open, partial] } }
# Field must be present
guards:
- { check: { field: approval_token, op: is_set } }
# Combine a check with a named Python guard
guards:
- { check: { field: retry_count, op: lt, value: 3 } }
- passes_risk_constraints
Built-in actions¶
PyStator ships 9 built-in actions registered by builtin_registries(). All work out of the box with zero custom Python. Actions that need external backends (publish, emit_metric) default to logging and can be swapped via configure_event_sink() / configure_metric_sink().
Context utilities¶
| Action | Params | Description |
|---|---|---|
noop |
— | Does nothing. Explicit placeholder or stub. |
log |
message, level |
Log a message (debug/info/warning/error). Defaults to transition summary at INFO. |
trace |
— | Append entry to ctx["_trace"] for test assertions. |
set_context |
any key-value pairs | Copy params into context. |
snapshot |
label (optional) |
Deep-copy non-internal context keys to ctx["_snapshots"]. |
copy_key |
from, to |
Copy value from one context key to another. Silent no-op if source absent. |
validate_keys |
required (list) |
Check keys exist and are not None. Logs warnings, sets ctx["_validation_errors"]. |
Integration¶
| Action | Params | Description |
|---|---|---|
publish |
topic (required), include (optional list) |
Emit a structured domain event to a configurable EventSink. Default: logs to pystator.events. |
emit_metric |
name (required), value, type, tags |
Emit a metric to a configurable MetricSink. Default: logs to pystator.metrics. |
Examples¶
# Capture context before a risky transition
actions:
- { name: snapshot, params: { label: "pre_execution" } }
- execute_orders
# Backup a value before overwriting
actions:
- { name: copy_key, params: { from: status, to: previous_status } }
- { set: { status: "processing" } }
# Validate required keys on state entry
on_enter:
- { name: validate_keys, params: { required: [order_id, quantity] } }
- process_order
# Publish a domain event with selected context keys
actions:
- { name: publish, params: { topic: "order.filled", include: [order_id, fill_qty] } }
# Emit a counter metric
actions:
- { name: emit_metric, params: { name: "orders.submitted", type: counter } }
Configuring custom sinks¶
The publish and emit_metric actions use pluggable sink protocols. Override the defaults in Python:
from pystator import builtin_registries, configure_event_sink, configure_metric_sink
guards, actions = builtin_registries() # logging defaults
configure_event_sink(actions, MyKafkaSink()) # swap to Kafka
configure_metric_sink(actions, MyDatadogSink()) # swap to Datadog
Your sink class just needs to implement EventSink.send(topic, payload, metadata) or MetricSink.emit(name, value, tags).
error_policy¶
Optional. Configures behavior on unhandled errors (e.g. guard raised, no matching transition).
| Field | Type | Default | Description |
|---|---|---|---|
default_fallback |
string | null |
State name to transition to on error. Must be a defined state (often type error). |
retry_attempts |
integer | 0 |
Number of retries (if supported by the runtime). Must be ≥ 0. |
state_variables¶
Optional. Defines the context contract for the machine: which keys are expected in the context dict passed to process() and used by guards/actions. When meta.validate_context is true, the engine validates context against this list (required keys, optional types) before each transition.
State variable item: long form¶
Each item can be an object:
| Field | Required | Type | Description |
|---|---|---|---|
key |
Yes | string | Context key name (e.g. order_id, quantity). Must be unique across the list. |
type |
No | string | Type hint: string, number, boolean, or object. Used for optional validation when validate_context is true. |
description |
No | string | Human-readable description. |
default |
No | any | Default value when building context (e.g. for UI "Fill from state variables"). |
required |
No | boolean | If true, context validation requires this key to be present (and not null) when validate_context is true. Default: false. |
Shorthand¶
You can pass a list of strings; each is treated as { "key": "<string>" }:
Example (long form)¶
state_variables:
- key: order_id
type: string
description: "Exchange order ID"
required: true
- key: quantity
type: number
default: 0
- key: filled_qty
type: number
default: 0
- key: retry_count
type: number
default: 0
description: "Validation retries (incremented declaratively)"
- key: phase
type: string
description: "Current processing phase (set declaratively on state entry)"
Validation¶
- Unique keys: No duplicate
keyvalues instate_variables(enforced by schema and semantic validator).
events¶
Optional documentation-only list of objects with name, optional description, payload. The engine does not use it for execution.
Complete example¶
The following shows groups, declarative effects, declarative checks, and spec refs. State names use lowercase identifiers (^[a-z0-9_]+$).
meta:
machine_name: optimization_cycle
version: "1.1.0"
description: "Single optimization-to-execution rebalance cycle"
strict_mode: true
groups:
data:
description: "Data collection and optimization"
validation:
description: "Validation and approval gate"
execution:
description: "Order execution and settlement"
terminal:
description: "Cycle outcomes"
state_variables:
- key: cycle_id
type: string
required: true
- key: retry_count
type: number
default: 0
description: "Validation-failure retries (incremented declaratively)"
- key: phase
type: string
description: "Current processing phase (set declaratively on each state entry)"
- key: failure_reason
type: string
description: "Set declaratively on failure or cancellation"
states:
- name: data_collection
type: initial
group: data
description: "Fetching market data and building optimization inputs"
on_enter:
- fetch_market_data
- { set: { phase: "data_collection" } }
- { timestamp: data_collection_started_at }
timeout:
seconds: 600.0
destination: failed
- name: validation
type: stable
group: validation
description: "Validating optimization results and risk constraints"
on_enter:
- validate_risk_constraints
- { set: { phase: "validation" } }
- name: failed
type: terminal
group: terminal
on_enter:
- notify_cycle_failed
- { set: { phase: "failed" } }
- { timestamp: cycle_failed_at }
transitions:
- trigger: validation_failed
source: validation
dest: data_collection
description: "Re-optimize with tighter constraints"
ref: "REQ-RETRY-001"
guards:
- { check: { field: retry_count, op: lt, value: 3 } }
actions:
- adjust_constraints
- { increment: retry_count }
- trigger: validation_failed
source: validation
dest: failed
description: "Validation failed, no retries remaining"
actions:
- { set: { failure_reason: "validation_exhausted" } }
Validation summary¶
- Schema (Pydantic): Structure and types are validated by
MachineConfig(seepystator.config.models). - Semantic (
ConfigValidator): - Exactly one state with
type: initial(when strict). - No duplicate state names.
- No duplicate
state_variablekeys instate_variables. - Every
sourceanddestin transitions must exist; timeoutdestinationmust exist;error_policy.default_fallbackmust exist if set. - No transitions from terminal states.
parentreferences an existing state; parent must not be aterminalstate.initial_childreferences an existing state whoseparentis this state.- Parallel
regions: each region’sinitialandstatesreference defined states; region names are unique machine-wide across parallel states. transition.region: names a defined region; each concretesourcestate belongs to that region.
Use from pystator import validate_config, ConfigValidator — then validate_config(config, strict=False) or ConfigValidator().validate(config) to get a list of error messages. With strict=True, validate_config raises ConfigurationError. Loading via StateMachine.from_yaml() / from_dict() runs validation when the loader is configured to validate.
See also¶
- Concepts — States, transitions, guards, actions, hierarchy, parallel.
- Quick start — First FSM and
process()call. - API reference —
StateMachine,validate_config, and execution.