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()); // trueState
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.
| Method | Description |
|---|---|
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"); // trueStates 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().
| Guard | Description |
|---|---|
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_COMPLETEStatus 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.
| Status | Meaning |
|---|---|
NOT_STARTED | Created but start() not called |
RUNNING | Active, accepting events |
COMPLETED | Reached a terminal state |
FAILED | Max transitions exceeded or error |
TIMEOUT | State timeout triggered |
sm.getStatus(); // Status.RUNNING
sm.isRunning(); // true
sm.isInTerminalState(); // false
sm.getTransitionCount(); // 2
sm.getAvailableEvents(); // ["SUBMIT"]
sm.reset(); // Back to NOT_STARTEDComprehensive 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);
}Context Management
Context window management, decision tracing, session history, knowledge extraction, automatic memory consolidation, and auto-summarization. These components help agents operate effectively within token limits and learn from past interactions.
Learning and Refinement
Feedback-driven learning, normative constraint enforcement, iterative refinement loops, prompt optimization, and structured output validation. These components enable agents to improve over time and produce higher-quality outputs.