Skip to content
tnsaiDocs
GitHub

State machines

StateMachine runs a fixed set of states and transitions: events move it from state to state, guards decide whether a transition may fire, and actions run on the way. It is in tnsai-intelligence, package com.tnsai.fsm.

A state machine is a standalone object. Agents do not create or drive one; you call it from your own code, for example inside an action.

Example

import com.tnsai.fsm.Event;
import com.tnsai.fsm.Guard;
import com.tnsai.fsm.State;
import com.tnsai.fsm.StateMachine;
import com.tnsai.fsm.Transition;

State idle = State.initial("IDLE");
State processing = State.of("PROCESSING");
State review = State.of("REVIEW");
State approved = State.terminal("APPROVED");
State rejected = State.terminal("REJECTED");

StateMachine sm = StateMachine.builder("ReviewWorkflow")
    .states(idle, processing, review, approved, rejected)
    .transition(Transition.from(idle).to(processing).on("START"))
    .transition(Transition.from(processing).to(review).on("SUBMIT"))
    .transition(Transition.from(review).to(approved)
        .on("REVIEW_COMPLETE")
        .when(Guard.greaterThan("confidence", 0.9)))
    .transition(Transition.from(review).to(rejected)
        .on("REVIEW_COMPLETE")
        .when(Guard.lessThan("confidence", 0.5)))
    .build();

sm.start();
sm.fire(Event.of("START"));
sm.fire(Event.of("SUBMIT"));
sm.getContext().set("confidence", 0.95);
sm.fire(Event.of("REVIEW_COMPLETE"));

sm.getCurrentState().getName(); // "APPROVED"
sm.getStatus();                 // COMPLETED

With a confidence of 0.7, neither guard passes: fire returns false and the machine stays in REVIEW.

States

FactoryMeaning
State.initial(name)Where start() begins. A machine needs exactly one.
State.of(name)An ordinary state
State.terminal(name)Entering it completes the machine

State.builder(name) adds initial(), terminal(), onEntry(ctx -> ...), onExit(ctx -> ...), timeout(ms) and metadata(key, value). Read metadata with getSpec(key).

Not implemented. timeout(ms) is stored and shown in diagrams, but nothing enforces it, and the TIMEOUT status is never set.

States are equal when their names are equal. Adding a second state with the same name replaces the first.

build() throws StateMachineException when there are no states, no initial state, or a transition that uses a state you did not add. A second initial state throws IllegalStateException. A terminal state is not required.

Transitions

Transition.from(review).to(approved)
    .on("REVIEW_COMPLETE")
    .when(Guard.greaterThan("confidence", 0.9))
    .action((ctx, from, to) -> ctx.set("approvedBy", "auto"))
    .priority(10)
    .build();
  • on(event) fires on an event with that name. automatic() fires without an event, whenever its guard passes.
  • when(guard) is optional; without one the transition always passes.
  • When several transitions from the current state match, the highest priority wins (default 0).
  • Transition.internal(state).on(event) goes from a state to itself. It still runs the state's exit and entry actions.

StateMachine.Builder.transition(...) takes a Transition or the unbuilt builder.

What a transition does

Firing a transition runs, in order: the source state's exit action, the transition action, and the target state's entry action. Listeners are notified at each step. Entering a terminal state sets the status to COMPLETED.

After start() and after every fire, automatic transitions run until none passes, the machine completes, or a state comes up twice in the same pass.

Each transition counts toward maxTransitions(n), 1000 by default. The transition after the limit sets the status to FAILED and throws StateMachineException.

An exception from a guard or an action propagates out of fire or start, and the status stays RUNNING.

Events

Event review = Event.of("REVIEW_COMPLETE")
    .withData("confidence", 0.95)
    .withData("reviewer", "agent-1");

double confidence = review.getData("confidence");
String notes = review.getDataOrDefault("notes", "none");
review.hasData("notes"); // false

Event is a record with a name and an immutable data map. withData returns a new event. During fire, the event is available to guards and actions as ctx.getCurrentEvent().

Context

StateMachineContext is the key-value store that guards and actions share. Pass one with initial data to start(context), or use sm.getContext():

MethodMeaning
get, getOrDefault, set, has, remove, clear, getDataThe values. set(key, null) throws NullPointerException; use remove.
getCurrentState(), getCurrentEvent()Where the machine is and what triggered it
setResult, getResultA result value for your own use
setError, getError, hasErrorAn error message. The machine sets it when the transition limit is hit.
getHistory()StateTransitionRecord(fromState, toState, event, timestamp) for every transition, starting with the entry into the initial state (fromState is null)

Guards

Guard is a functional interface, evaluate(context):

GuardPasses when
Guard.always(), Guard.never()Always, never
Guard.hasKey(key)The context has the key
Guard.equals(key, value)The context value equals value
Guard.greaterThan(key, n), Guard.lessThan(key, n)The context value is a number above or below n. A missing value fails.
Guard.eventDataEquals(key, value)The current event's data has that value

Combine guards with and, or and negate, or write a lambda:

Guard safe = Guard.greaterThan("confidence", 0.8).and(Guard.lessThan("risk", 0.3));
Guard notBlocked = Guard.hasKey("blocked").negate();
Guard authenticated = ctx -> ctx.has("userId");

Machine status

StatusMeaning
NOT_STARTEDBefore start(), and after reset()
RUNNINGAccepting events
COMPLETEDA terminal state was entered
FAILEDThe transition limit was exceeded
TIMEOUTNever set

fire throws StateMachineException unless the status is RUNNING, so firing after completion is an error. start throws if the machine was already started. reset() clears the context, the current state and the transition count, and sets the status back to NOT_STARTED.

isRunning(), isInTerminalState(), getTransitionCount(), getStates() and getTransitions() describe the machine. getAvailableEvents() lists the event names of the transitions from the current state, without checking their guards.

Listeners

import com.tnsai.fsm.StateMachineListener;

sm.addListener(new StateMachineListener() {
    @Override
    public void onTransition(StateMachine sm, State from, State to, Event event) {
        System.out.println(from.getName() + " -> " + to.getName());
    }
});

sm.addListener(StateMachineListener.logging());

StateMachineListener has default no-op methods: onStateEntered, onStateExited, onTransition, onNoTransition and onCompleted. The event is null for automatic transitions and for the entry into the initial state. StateMachineListener.logging() logs entries, transitions and completion at INFO through SLF4J.

Diagrams

StateMachineVisualizer turns a machine into text:

MethodOutput
toMermaid(sm), toMermaid(sm, MermaidOptions)A Mermaid stateDiagram-v2
toDot(sm), toDot(sm, DotOptions)Graphviz DOT, terminal states as double circles
toText(sm)Status, current state, states and transitions

For the example above, toMermaid(sm) gives:

stateDiagram-v2
    %% ReviewWorkflow
    [*] --> IDLE
    IDLE --> PROCESSING : START
    PROCESSING --> REVIEW : SUBMIT
    REVIEW --> APPROVED : REVIEW_COMPLETE [guarded]
    REVIEW --> REJECTED : REVIEW_COMPLETE [guarded]
    APPROVED --> [*]
    REJECTED --> [*]
    class APPROVED currentState

States are not listed in the order you added them. Transitions are.

MermaidOptionsDefault
includeTitletrue
includeStateDescriptions (timeouts)true
showGuardstrue
showAutoTransitionstrue
highlightCurrentStatetrue. It adds a currentState class but no style for it.
DotOptionsDefault
direction"LR"
nodeShape"ellipse"
fontName"Arial"
showGuards, showAutoTransitions, highlightCurrentStatetrue
currentStateColor"#90EE90"

Thread safety

StateMachine is not thread-safe. The context and the listener list are concurrent collections, but fire, start and reset are not synchronized. Fire events from one thread, or synchronize around the machine.

  • Planning: planners that choose actions from goals instead of fixed transitions.
  • Intelligence: the other parts of tnsai-intelligence.