TnsAI
Intelligence

Finite State Machine

Deterministic state machine for bounded agent autonomy. Provides guard-based transitions, entry/exit actions, automatic transitions, event payloads, listeners, and visualization to Mermaid and Graphviz DOT.

Quick Start

This example builds a simple review workflow with five states and guarded transitions. The machine starts in IDLE, moves through PROCESSING and REVIEW, and ends in either APPROVED or REJECTED based on a confidence score.

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)))
    .maxTransitions(100)
    .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"));

System.out.println(sm.getCurrentState().getName()); // "APPROVED"
System.out.println(sm.isInTerminalState());          // true

State

States can be initial, terminal, or intermediate. Each supports entry/exit actions, timeouts, and metadata.

Factory Methods

Use these to create states quickly. Every state machine needs exactly one initial state, and at least one terminal state where the machine stops.

MethodDescription
State.initial(name)Entry point -- exactly one per machine
State.of(name)Regular intermediate state
State.terminal(name)End state -- machine completes here

Builder

For more control, the builder lets you attach entry/exit actions (code that runs when a state is entered or left), set timeouts, and store metadata on the state.

State processing = State.builder("PROCESSING")
    .initial()                                         // Mark as initial
    .onEntry(ctx -> ctx.set("startTime", System.currentTimeMillis()))
    .onExit(ctx -> {
        long duration = System.currentTimeMillis() - (long) ctx.get("startTime");
        ctx.set("processingDuration", duration);
    })
    .timeout(30_000)                                   // 30 second timeout (ms)
    .metadata("retryable", true)
    .build();

processing.isInitial();        // true
processing.isTerminal();       // false
processing.hasTimeout();       // true
processing.getTimeoutMs();     // 30000
processing.getSpec("retryable"); // true

States are identified by name. Two states with the same name are considered equal.

Transition

Transitions define movement between states, triggered by events with optional guards, actions, and priority.

// Simple transition
Transition t1 = Transition.from(idle).to(processing).on("START").build();

// With guard and priority
Transition t2 = Transition.from(review).to(approved)
    .on("REVIEW_COMPLETE")
    .when(Guard.greaterThan("confidence", 0.9))
    .priority(10)
    .build();

// With transition action
Transition t3 = Transition.from(review).to(rejected)
    .on("REVIEW_COMPLETE")
    .when(Guard.lessThan("confidence", 0.5))
    .action((ctx, from, to) -> ctx.set("reason", "Low confidence"))
    .build();

// Automatic transition (no event required, fires when guard passes)
Transition auto = Transition.from(processing).to(review)
    .automatic()
    .when(Guard.equals("processed", true))
    .build();

// Internal (self-loop) transition
Transition internal = Transition.internal(processing)
    .on("RETRY")
    .action((ctx, from, to) -> {
        int count = ctx.getOrDefault("retries", 0);
        ctx.set("retries", count + 1);
    })
    .build();

When multiple transitions match the same event from the same state, the one with the highest priority wins.

Event

Events are the signals that drive the state machine forward. When you fire an event, the machine looks for a matching transition from the current state. Events can carry key-value payload data that guards and actions can read.

Event start = Event.of("START");

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

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

sm.fire(review);

StateMachineContext

The context is a shared key-value store that guards, actions, and listeners can all read and write. Use it to pass data between states, track results, record errors, and review the full transition history.

StateMachineContext ctx = new StateMachineContext(Map.of("userId", "u123"));

ctx.set("processed", true);
ctx.get("userId");                        // "u123"
ctx.getOrDefault("retries", 0);           // 0
ctx.has("processed");                     // true
ctx.remove("processed");

ctx.setResult("Task completed");
String result = ctx.getResult();
ctx.setError("Timeout exceeded");
ctx.hasError();                           // true

// Transition history
List<StateTransitionRecord> history = ctx.getHistory();
for (var record : history) {
    System.out.printf("%s -> %s via %s%n",
        record.fromState(), record.toState(), record.event());
}

// Start machine with pre-populated context
sm.start(new StateMachineContext(Map.of("threshold", 0.8)));

Guard

Guards are conditions that must be true for a transition to fire. They check the context (shared state) and optionally the event payload. If a guard returns false, the transition is skipped even if the event matches. TnsAI provides a rich set of built-in guards plus composition operators so you can combine them.

Built-in Guards

These factory methods cover the most common conditions. For anything more complex, use a lambda or compose built-in guards with .and(), .or(), and .negate().

GuardDescription
Guard.always()Always true (default for transitions)
Guard.never()Always false
Guard.hasKey("k")True if key exists in context
Guard.equals("k", val)True if context value equals expected
Guard.greaterThan("k", 0.9)Numeric comparison (>)
Guard.lessThan("k", 0.5)Numeric comparison (<)
Guard.eventDataEquals("k", val)Check current event's payload

Custom and Composed Guards

You can write guards as lambdas or compose built-in guards using .and(), .or(), and .negate() for complex conditions.

// Lambda guard
Guard isAuthenticated = ctx -> ctx.has("userId");

// Event data guard
Guard approvedStatus = ctx -> {
    Event event = ctx.getCurrentEvent();
    return "APPROVED".equals(event.getData("status"));
};

// AND composition
Guard safe = Guard.greaterThan("confidence", 0.8)
    .and(Guard.lessThan("risk", 0.3));

// OR composition
Guard acceptable = Guard.equals("role", "admin")
    .or(Guard.equals("role", "moderator"));

// Negation
Guard notBlocked = Guard.hasKey("blocked").negate();

StateMachineListener

Listeners let you observe everything that happens in the state machine without modifying its behavior. Use them for logging, metrics, debugging, or triggering side effects when specific transitions occur.

sm.addListener(new StateMachineListener() {
    @Override
    public void onStateEntered(StateMachine sm, State state, Event event) {
        System.out.println("Entered: " + state.getName());
    }

    @Override
    public void onTransition(StateMachine sm, State from, State to, Event event) {
        String trigger = event != null ? event.getName() : "auto";
        System.out.printf("%s -> %s [%s]%n", from.getName(), to.getName(), trigger);
    }

    @Override
    public void onNoTransition(StateMachine sm, State state, Event event) {
        System.out.println("Unhandled event: " + event.getName());
    }

    @Override
    public void onCompleted(StateMachine sm) {
        System.out.println("Completed in: " + sm.getCurrentState().getName());
    }
});

// Built-in logging listener (SLF4J)
sm.addListener(StateMachineListener.logging());
// Logs: [FSM:ReviewWorkflow] IDLE -> PROCESSING [START]

StateMachineVisualizer

Export your state machine as a diagram for documentation, debugging, or sharing. Three output formats are supported: Mermaid (renders in GitHub, Notion, etc.), Graphviz DOT (for offline rendering), and plain text.

Mermaid

Generates a Mermaid state diagram that renders natively on GitHub, GitLab, and many documentation tools.

String mermaid = StateMachineVisualizer.toMermaid(sm);
// stateDiagram-v2
//     %% ReviewWorkflow
//     [*] --> IDLE
//     IDLE --> PROCESSING : START
//     PROCESSING --> REVIEW : SUBMIT
//     REVIEW --> APPROVED : REVIEW_COMPLETE [guarded]
//     REVIEW --> REJECTED : REVIEW_COMPLETE [guarded]
//     APPROVED --> [*]
//     REJECTED --> [*]

// With options
String mermaid = StateMachineVisualizer.toMermaid(sm,
    MermaidOptions.defaults()
        .includeTitle(true)
        .includeStateDescriptions(true)
        .showGuards(true)
        .showAutoTransitions(true)
        .highlightCurrentState(true));

Graphviz DOT

Generates DOT format for use with Graphviz or compatible rendering tools, with configurable layout direction, colors, and fonts.

String dot = StateMachineVisualizer.toDot(sm);

String dot = StateMachineVisualizer.toDot(sm,
    DotOptions.defaults()
        .direction("LR")               // LR, TB, RL, BT
        .nodeShape("ellipse")
        .fontName("Helvetica")
        .currentStateColor("#90EE90")
        .showGuards(true));

Plain Text

A simple human-readable summary of the machine's states, transitions, and current status -- useful for logging and console output.

String text = StateMachineVisualizer.toText(sm);
// State Machine: ReviewWorkflow
// Status: COMPLETED
// Current State: APPROVED
// Transitions: 3
//
// States:
//   - IDLE [initial]
//   - PROCESSING
//   - REVIEW
//   - APPROVED [terminal]
//   - REJECTED [terminal]
//
// Transitions:
//   - IDLE -> PROCESSING on START
//   - PROCESSING -> REVIEW on SUBMIT
//   - REVIEW -> APPROVED on REVIEW_COMPLETE
//   - REVIEW -> REJECTED on REVIEW_COMPLETE

Status Lifecycle

A state machine moves through these statuses during its lifetime. You can query the current status at any time to decide whether to fire more events or handle completion.

StatusMeaning
NOT_STARTEDCreated but start() not called
RUNNINGActive, accepting events
COMPLETEDReached a terminal state
FAILEDMax transitions exceeded or error
TIMEOUTState timeout triggered
sm.getStatus();          // Status.RUNNING
sm.isRunning();          // true
sm.isInTerminalState();  // false
sm.getTransitionCount(); // 2
sm.getAvailableEvents(); // ["SUBMIT"]
sm.reset();              // Back to NOT_STARTED

Comprehensive Example

This example models a real-world order processing workflow with automatic transitions, validation guards, cancellation handling, and logging. It demonstrates how all the FSM building blocks fit together.

// Order processing workflow
State pending = State.builder("PENDING").initial()
    .onEntry(ctx -> log.info("Order received")).build();
State validating = State.builder("VALIDATING")
    .timeout(10_000).build();
State payment = State.of("PAYMENT");
State fulfilled = State.terminal("FULFILLED");
State cancelled = State.terminal("CANCELLED");

StateMachine orderFSM = StateMachine.builder("OrderProcess")
    .states(pending, validating, payment, fulfilled, cancelled)
    .transition(Transition.from(pending).to(validating).automatic()
        .when(Guard.hasKey("orderId")))
    .transition(Transition.from(validating).to(payment)
        .on("VALIDATED").when(Guard.equals("valid", true)))
    .transition(Transition.from(validating).to(cancelled)
        .on("VALIDATED").when(Guard.equals("valid", false))
        .action((ctx, from, to) -> ctx.set("reason", "Validation failed")))
    .transition(Transition.from(payment).to(fulfilled)
        .on("PAYMENT_RESULT").when(Guard.equals("paid", true)))
    .transition(Transition.from(payment).to(cancelled)
        .on("PAYMENT_RESULT").when(Guard.equals("paid", false)))
    .transition(Transition.from(validating).to(cancelled).on("CANCEL"))
    .transition(Transition.from(payment).to(cancelled).on("CANCEL"))
    .maxTransitions(50)
    .build();

orderFSM.addListener(StateMachineListener.logging());

StateMachineContext ctx = new StateMachineContext(Map.of("orderId", "ORD-123"));
orderFSM.start(ctx);                          // PENDING -> VALIDATING (auto)
ctx.set("valid", true);
orderFSM.fire(Event.of("VALIDATED"));          // -> PAYMENT
ctx.set("paid", true);
orderFSM.fire(Event.of("PAYMENT_RESULT"));     // -> FULFILLED

System.out.println(StateMachineVisualizer.toMermaid(orderFSM));

Thread Safety

StateMachine is not thread-safe by design to keep the implementation simple and fast. If your application fires events from multiple threads, you need to synchronize access externally.

synchronized (sm) {
    sm.fire(event);
}

On this page