Skip to content

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

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-zA-Z][a-zA-Z0-9_.]*$.
type string One of: initial, stable, terminal, error, parallel. 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 when strict_mode is 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 with error_policy.default_fallback for unhandled errors.
  • parallel — Container for orthogonal regions. Must define regions; 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

timeout:
  seconds: 30.0
  destination: TIMED_OUT
  • seconds — Float, must be > 0. After this many seconds the machine can transition to destination (typically used with a timeout manager or scheduler).
  • destination — State name. Must be a defined state.

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-zA-Z][a-zA-Z0-9_.]*$.
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:

guards:
  - data_quality_sufficient
  - passes_risk_constraints

Inline expression — evaluated against context variables; requires pystator[recipes]:

guards:
  - { expr: "fill_qty >= order_qty" }

Declarative check — structured field comparison evaluated by the engine with no Python required:

guards:
  - { check: { field: retry_count, op: lt, value: 3 } }

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:

actions:
  - store_optimal_weights
  - notify_cycle_completed

Parameterized action — named action with extra params passed via context:

actions:
  - { name: send_alert, params: { severity: warning, channel: ops } }

Declarative effect — inline context mutation evaluated by the engine with no Python required:

actions:
  - { set: { approved_by: "auto" } }
  - { timestamp: approved_at }

See Declarative effects for all effect types and examples.

Validation rules for transitions

  • Every state name in source and dest must be defined in states.
  • No transition may have a terminal state in source.
  • For delayed transitions (after), a scheduler must be configured at runtime (e.g. Orchestrator with AsyncioScheduler, RedisScheduler, or CeleryScheduler).

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

- { set: { phase: "collecting", status: "active" } }

Calls ctx.update(...). Accepts a mapping of one or more key-value pairs.

timestamp — record current UTC time

- { timestamp: collection_started_at }

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

- { increment: retry_count }

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

- { decrement: remaining_attempts }

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

- { append: { field: filled_order_ids, value: "ord-123" } }

If the field is absent a new list is created. Equivalent to ctx.setdefault(field, []).append(value).

clear — remove a field from context

- { clear: active_cycle_id }

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:

guards:
  - { check: { field: <field_name>, op: <operator>, value: <value> } }

For membership operators (in, not_in), use values (a list) instead of value:

guards:
  - { check: { field: status, op: in, values: [OPEN, PENDING] } }

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>" }:

state_variables: [order_id, quantity, filled_qty]

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 key values in state_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 all features together: groups, state grouping, declarative effects, declarative checks, and spec refs.

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:
  # Declarative check: allow up to 3 retries without a Python guard
  - 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 }

  # Named Python guard + declarative effects
  - 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 (see pystator.config.models).
  • Semantic (ConfigValidator):
  • Exactly one state with type: initial (when strict).
  • No duplicate state names.
  • No duplicate state_variable keys in state_variables.
  • Every source and dest in transitions must exist; timeout destination must exist; error_policy.default_fallback must exist if set.
  • No transitions from terminal states.

Use validate_config(config) or ConfigValidator().validate(config) to get a list of error messages, or load via StateMachine.from_yaml() / from_dict() which validate on load.


See also

  • Concepts — States, transitions, guards, actions, hierarchy, parallel.
  • Quick start — First FSM and process() call.
  • API referenceStateMachine, validate_config, and execution.