State Machine Basics: An AI Chat Example

This is the technical companion to Why Use State Machines?, written for developers. It walks through one small state machine map from my framework, explains what happens when things go wrong, and lists all the building blocks (primitives) and how they are stored.

The Example Map: AI Chat Initiation

Initial State   | ⚡ A-RESET_CONTEXT-A
with Entry and  | ◯ S-IDLE-S
Exit Actions    | ⚡ A-LOG_IDLE_EXIT-A
                | |
                | +-------------------------+
                | |                         |
2 Transitions   | 🛑 G-CONFIG_READY-G       🛑 G-CONFIG_MISSING-G
with Guards and | 🔔 E-START_CHAT_REQ-E     🔔 E-START_CHAT_REQ-E
Actions         | ⚡ A-INIT_CONNECTION-A    ⚡ A-OPEN_SETUP_DIALOG-A
                | |                         |
                | V                         V
2 States        | ◯ S-CHAT_ACTIVE-S         ◯ S-AWAITING_SETUP-S
                |                           |
                |                           V
1 Transition    |                           🛑 G-INPUT_VALID-G
with Guards and |                           🔔 E-SETUP_COMPLETE-E
Actions         |                           ⚡ A-INIT_CONNECTION-A
                |                           |
                |                           V
1 State         |                           ◯ S-CHAT_ACTIVE-S

Each name carries its type in a prefix and suffix: S-…-S for states, E-…-E for events, G-…-G for guards, and A-…-A for actions.

The Four Main Primitives

States (◯ S-NAME-S)

These represent the different modes or conditions of the application. The machine is always in exactly one state.

Guards (🛑 G-NAME-G)

Logical conditions evaluated before a transition is taken. They act as a “Lookahead” gate; if the guard returns False, the transition is blocked. [Only one guard per transition]

Events (🔔 E-NAME-E)

The triggers that tell the machine it is time to transition to a new state (e.g., a user clicking a button).

Actions (⚡ A-NAME-A)

The functional “Muscles” that perform work. In this system, they are categorized by their placement in the sequence:

Entry Actions: Located above their state in the map above (the left “ear” in the graphical map editor); they fire automatically upon entering the state.

Exit Actions: Located below their state in the map above (the right “ear” in the graphical map editor); they fire automatically before leaving the state.

Transition Actions: Located on the transition line (below the guard/event stack); they fire only when moving between states.

Walking Through the Example

This follows the life cycle of the AI Chat Initiation state machine, describing how the application moves from resting to active communication.

1. The Resting Phase

The machine begins in ◯ S-IDLE-S. Because of its entry action (⚡ A-RESET_CONTEXT-A), the machine automatically wipes its internal memory every time it returns here, ensuring no stale IP addresses or provider settings linger from previous sessions.

2. The Trigger

The silence is broken when the user clicks “Start Chat,” which rings the 🔔 E-START_CHAT_REQ-E event. As the machine prepares to leave the idle state, it executes its exit action (⚡ A-LOG_IDLE_EXIT-A), recording the timestamp of the request for the system logs.

3. The Logical Fork

The machine now stands at a crossroads. It evaluates the guards of both transitions, in priority order, to decide its path:

4. The Interaction Detour

While the machine is in ◯ S-AWAITING_SETUP-S, the system pauses and a dialog pops up. It remains here until the user provides the missing IP, Provider, and Model. Once the user clicks “Finish,” a new event occurs: 🔔 E-SETUP_COMPLETE-E.

5. Final Validation & Success

Before leaving the ◯ S-AWAITING_SETUP-S state and initiating the chat, the machine performs one last check. It evaluates the 🛑 G-INPUT_VALID-G guard to ensure the user didn’t leave any fields blank or enter an invalid IP. If the data passes this gate, the machine executes the connection muscle (⚡ A-INIT_CONNECTION-A) and joins the user in the ◯ S-CHAT_ACTIVE-S state.

Result: The user is now connected to the AI. Because of the FSM’s design, it was impossible to reach the “Active” state without a valid configuration: missing or invalid settings are stopped before a connection is even attempted. (A connection that fails for other reasons, such as a timeout, is covered below under Logic Failure.)

What If an Action Fails?

If an action like A-INIT_CONNECTION-A fails, is the transition still completed? In this architecture, the answer depends on how the action fails:

1. Hard Failure (Runtime Crash)

If A-INIT_CONNECTION-A throws a Python exception (e.g., a network library crashes or a variable is missing), the transition is aborted and rolled back.

2. Logic Failure (Business Logic)

If the action executes successfully but “fails” to connect (e.g., a timeout or wrong password), the transition is completed, but the action returns a specific feedback key.

3. Pre-condition Failure (Contract Violation), Not Shown on the Map

If the action has a contract (e.g., it requires global_ip_address to be set) and that data is missing:

In short: If the action fails to start or crashes, the machine stays “safe” by rolling back. If it starts but the network fails, it completes the transition and then uses its internal logic to handle the error event.

Forks: One Guard, or Two Guards That Are Both True

In this FSM, there’s a logical fork based on guards. What if only one of those guards is set up, or both are true at the same time?

In this system, a fork is handled by the Determinism Law. Here is what happens in those two specific edge cases:

1. What If Only One Guard Is Set Up? (The “Lookahead” Barrier)

If you only have one transition for 🔔 E-START_CHAT_REQ-E and its 🛑 guard returns False:

2. What If Both Guards Are True? (The Priority Tie-Breaker)

If you have two transitions for the same event and both their 🛑 guards return True at the same time, the machine has a “collision.” To resolve this, the engine uses priority numbers:

3. The Linter’s Role (Gate 2: Determinism)

Because two transitions that can both be taken at once are usually a logic error (why have a fork if both paths can be taken?), the system includes an Auditor (the linter and rigor gates that run when a map is exported) that checks for this:

Summary:

Order of Operations When Leaving a State

When exiting a state, does the exit action fire before the guard for the next transition is checked?

In this architecture, the 🛑 guard is checked BEFORE the ⚡ exit action fires.

This is a fundamental law of the system: the engine must decide if the transition is even possible before it begins the destructive process of leaving the current state.

The Atomic Order of Operations

  1. 🔔 Event Rings: Stimulus arrives.
  2. 🛑 Guard Evaluation (The Gate): While still fully inside the source state, the engine checks the guard.
    • If False: Nothing happens. No actions fire. The machine remains in the current state.
    • If True: The transition is “authorized,” and the following sequence is locked in:
  3. ⚡ Exit Actions (the right ear in the map editor): The machine fires the exit actions of the state it is leaving.
  4. ⚡ Transition Actions (below the event): The machine fires the actions associated with the move itself.
  5. ◯ State Update: The machine formally changes its internal pointer to the target state.
  6. ⚡ Entry Actions (the left ear in the map editor): The machine fires the entry actions of the new state.

Why This Order Matters

Imagine a “Transfer Funds” state. If the ⚡ exit action fired before the 🛑 guard was checked, you might deduct a service fee from a user’s account only to have the guard then realize the user doesn’t have permission to complete the transfer.

By checking the 🛑 guard first, the machine guarantees that no side effects (exit, transition, or entry actions) occur unless the move has been authorized.

Summary

The Primitives

Here are the primitives of the system, in their three functional groups with their strict relational owners. Each one is a table in the map database.

Group 1: The Core Structure (The Map)

These primitives define the physical layout and movement of the state machine.

Primitive Description Owner
Atlas The root identity and container for a single machine. System Root
State A discrete mode or condition the machine can be in. Atlas
Event A signal or trigger that initiates a state change. Atlas
Action A physical Python implementation (“Muscle”) that does work. Atlas
Transition The relational path connecting two states via an event. Source State

Group 2: The Logic Context (The Brain)

These primitives define how the machine thinks, remembers, and maintains consistency.

Primitive Description Owner
Variable A memory register used to store primitive data. Atlas
Guard A logic gate evaluated before a transition is taken. Atlas
Memory Link Action Defines which Variables an Action is allowed to access. Action
Memory Link Guard Defines which Variables a Guard requires for its decision. Guard
Feedback Link Maps an Action’s result to a new internal Event. Action
Global Invariant A universal law that must be true at all times. Checked after every Action. Atlas

Group 3: The Governance & UI Layer (The Rules)

These primitives define the external interaction and the contracts that protect the code.

Primitive Description Owner
Widget A UI component that can emit events or display data. Atlas
Property A visual attribute (like “Visible”) that can be toggled. Atlas
Manifest A policy determining UI state based on the current machine mode. State
Pre-condition A requirement that must be true before an Action is called. Action
Post-condition A promise of a variable change after an Action succeeds. Action

Summary of Ownership

The Atlas owns the definitions (The “Vocabulary”).

The State owns the experience (The “Policy” and “Paths”).

The Action owns the execution (The “Requirements” and “Permissions”).

Can a Primitive Have More Than One Owner?

The FSM maps are modeled in a strictly normalized relational database. Every record has exactly one primary owner (a foreign key that dictates its parent). However, from a functional perspective, some primitives have “Co-Ownership” because they sit at the intersection of two concepts.

Here are the three primitives that exhibit Functional Co-Ownership:

1. The Transition (Owned by State AND Event)

2. The Manifest Rule (Owned by State AND Widget AND Property)

Why This Matters for the Primitives

The system uses ON DELETE CASCADE (the database automatically deletes a record’s dependent records) to enforce these co-ownership rules.

If you delete an Atlas, everything is wiped.
If you delete an Action, its Pre-conditions, Post-conditions, Memory Links, and Feedback Links are automatically erased because they have no functional purpose without their “Executing Owner.”

In short: While the database assigns a “Parent” for record-keeping, the system logic treats these primitives as Contracts between two parties. If either party leaves the system, the contract is torn up.