What are State Machines?

What are State Machines?

A state machine is a way of modeling systems that can only be in one state at a time—and change states in response to events.

Consider a turnstile at a subway station: it's either locked or unlocked. Insert a coin, and it unlocks. Push through, and it locks again. It can't be both locked and unlocked simultaneously—and every input has a defined outcome depending on the current state.

                    coin
          ┌──────────────────────┐
          ▼                      │
    ┌───────────┐          ┌───────────┐
    │  UNLOCKED │          │  LOCKED   │
    └───────────┘          └───────────┘
          │                      ▲
          └──────────────────────┘
                    push

This simple concept scales to surprisingly complex systems: UI components, network protocols, game AI, robotics, workflow engines, and more. By forcing you to explicitly define every state and valid transition, state machines make complex behavior predictable, testable, and maintainable.


Why Explicit State Machines Beat Implicit State

Most code manages state implicitly—through scattered boolean flags, nested conditionals, and variables that interact in subtle ways. This works for simple cases but quickly becomes fragile for complex scenarios. Even thorough testing can't catch every possible state combination. This leads to bugs, unexpected behavior, and maintenance nightmares.

The Implicit Approach (Fragile)

javascript
let isLoading = false;
let isError = false;
let isSuccess = false;
let data = null;

async function fetchData() {
  isLoading = true;
  isError = false;  // don't forget to reset this!
  try {
    data = await api.get();
    isSuccess = true;
    isLoading = false;  // what if we forget this line?
  } catch (e) {
    isError = true;
    isLoading = false;
    isSuccess = false;  // is this state even possible now?
  }
}

Problems with implicit state:

  • Impossible states are possible. Nothing prevents isLoading and isSuccess from both being true. The compiler won't help you; only careful programming will—and this doesn't scale well.
  • State transitions are scattered. To understand what happens on an error, you have to trace through the entire codebase looking for every place that sets isError = true.
  • Adding states creates combinatorial explosions. Three booleans = 8 possible combinations. Four booleans = 16. Most of these combinations are meaningless bugs waiting to happen.
  • Testing is guesswork. Which combinations do you even need to test? You don't have a clear list of valid states.
  • Extra cognitive load. Developers must mentally track how multiple flags interact. This leads to mistakes and misunderstandings. Only developers familiar with the code can modify it without breaking things.
  • Domain logic gets buried. Business rules are hidden in conditionals scattered throughout the code, making it hard to see the big picture. Domain experts without programming skills can't easily validate the logic.

The Explicit Approach (Robust)

javascript
// State can ONLY be one of these values
let state = 'idle'; // 'idle' | 'loading' | 'success' | 'error'
let data = null;

const transitions = {
  idle:    { FETCH: 'loading' },
  loading: { SUCCESS: 'success', FAILURE: 'error' },
  success: { FETCH: 'loading' },
  error:   { FETCH: 'loading', RESET: 'idle' }
};

function send(event) {
  const nextState = transitions[state]?.[event];
  if (nextState) {
    state = nextState;
  }
  // Invalid transitions are simply ignored—no corruption possible
}

async function fetchData() {
  send('FETCH');
  try {
    data = await api.get();
    send('SUCCESS');
  } catch (e) {
    send('FAILURE');
  }
}

Why this is superior:

  • Impossible states are impossible. The system is in exactly one state. There's no isLoading && isSuccess bug because that combination cannot be expressed.
  • All states are visible at a glance. Open the file, see the states. No hunting through code.
  • Transitions are explicit and documented. The transitions object is a complete specification of what can happen and when. It's self-documenting.
  • Adding states is additive, not multiplicative. A new state adds one row to the table, not 2ⁿ new combinations to worry about.
  • Testing is systematic. You can enumerate every state and every valid transition. Full coverage is achievable and verifiable.
  • Debugging is straightforward. Log the current state and the event. That's all you need to reproduce any issue.
  • Domain logic is centralized. Business rules are clearly defined in one place, making it easier for domain experts to review and validate. Using a visual representation (like state diagrams) further aids understanding and allows non-developers to grasp the system's behavior.

The Limitation: Manual Coding Still Has Problems

The explicit approach is a huge improvement, but hand-coding state machines still has friction:

javascript
const transitions = {
  idle:    { FETCH: 'loading' },
  loading: { SUCCESS: 'sucess', FAILURE: 'error' },  // typo: 'sucess'
  success: { FETCH: 'loading' },
  error:   { FETCH: 'loading', RESET: 'idle' }
};

Remaining pain points:

  • Typos compile fine. The typo 'sucess' above won't cause any error until runtime—and even then, it might silently fail. String-based state names offer no compile-time safety.
  • No visual overview. As machines grow, the code becomes harder to reason about. You're reading a table, not seeing the flow.
  • Manual synchronization. Need the same logic in Java, JavaScript, and C? You're maintaining three separate implementations. Drift is inevitable.
  • Testing requires integration. You can't easily test the state machine in isolation without wiring up the full application.
  • Advanced features are complex. Hierarchy, parallel states, history, delayed events—implementing these correctly by hand is error-prone and time-consuming.

The Solution: Visual Design, Interactive Testing, Code Generation

This is where the vscxml toolchain transforms your workflow:

┌───────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Visual Editor    │────►│    Simulator    │────►│    Generator    │
│                   │     │                 │     │                 │
│  Design states    │     │  Test behavior  │     │  Java / JS / C  │
│  Draw transitions │     │  Send events    │     │  Zero overhead  │
│  See the flow     │     │  Debug live     │     │  Type-safe      │
└───────────────────┘     └─────────────────┘     └─────────────────┘
         │                         │                       │
     No coding              No integration          No translation
     required               required                errors

Visual Editor — Design state machines by drawing them:

  • Drag and drop states onto a canvas
  • Draw transitions with your mouse
  • See the entire machine at a glance—not as code, as a diagram
  • Automatic validation catches unreachable states, missing transitions, duplicate IDs
  • Export to standard W3C SCXML format

Simulator — Test before you write application code:

  • Load your SCXML and start sending events interactively
  • Watch state transitions happen in real-time
  • Inspect datamodel variables as they change
  • Find bugs in your state logic before they reach production
  • No application scaffolding required—test the machine in isolation

Code Generator — One source, multiple targets:

  • Generate optimized Java, JavaScript, or C from the same SCXML
  • Transpiled output—no runtime XML parsing overhead
  • Type-safe constants for states and events (no string typos)
  • Guaranteed consistency across platforms
  • Advanced features (hierarchy, parallel, history, invoke) work out of the box

The workflow in practice:

  1. Design your state machine visually in the editor
  2. Test it interactively in the simulator until the behavior is correct
  3. Generate production code for your target platform
  4. Integrate the generated code into your application

The state machine logic is defined once, visualized clearly, tested thoroughly, and deployed everywhere—with zero manual translation and zero runtime interpretation overhead.


Hierarchical State Machines (Statecharts)

Flat state machines work well for simple systems, but they hit a wall when behavior is shared across states. This is where hierarchy comes in—states can contain substates, and behavior defined on a parent state applies to all its children.

The Problem: Repeated Transitions

Imagine a media player with three states: playing, paused, and stopped. From any of these states, the user can press the power button to turn the device off.

┌───────────┐  power   ┌───────────┐
│  PLAYING  │ ───────► │    OFF    │
└───────────┘          └───────────┘
                             ▲  ▲
┌───────────┐  power         │  │
│  PAUSED   │ ───────────────┘  │
└───────────┘                   │
                                │
┌───────────┐  power            │
│  STOPPED  │ ──────────────────┘
└───────────┘

Now add fast-forward, rewind, and buffering states. Each one needs its own power → off transition. This duplication is tedious and error-prone.

The Solution: Hierarchy

Group related states under a parent. The parent handles shared behavior; children handle specific behavior.

┌─────────────────────────────────────────────────┐
│                      ON                         │
│  ┌───────────┐    ┌───────────┐    ┌─────────┐  │
│  │  PLAYING  │◄──►│  PAUSED   │◄──►│ STOPPED │  │
│  └───────────┘    └───────────┘    └─────────┘  │
│         ▲                                       │
│         └──── play/pause/stop events ────       │
└─────────────────────────┬───────────────────────┘
                          │ power
                          ▼
                    ┌───────────┐
                    │    OFF    │
                    └───────────┘

Now the power event is handled once at the ON state level. All substates inherit this behavior automatically. When you add fast-forward or buffering, they just become new substates inside ON—no additional power transitions needed.

Benefits of hierarchy:

  • Eliminates duplication. Common transitions are defined once on the parent.
  • Encapsulates complexity. The ON state is a black box from the outside. External code doesn't need to know whether you're playing or paused.
  • Mirrors real-world structure. Systems naturally have "modes" with submodes. Hierarchy lets you model this directly.
  • Scales gracefully. You can nest hierarchies as deep as needed without the state explosion of flat machines.

This hierarchical extension of state machines is called a statechart, formalized by David Harel in 1987, and is the foundation of specifications like SCXML.


Guards: Conditional Transitions

Sometimes a transition should only occur if certain conditions are met. A guard is a boolean expression that must evaluate to true for a transition to fire. If the guard is false, the transition is ignored—as if the event never happened.

The Problem: Transitions That Shouldn't Always Fire

Consider an ATM. When the user requests a withdrawal, the machine shouldn't blindly dispense cash—it needs to check if the account has sufficient funds.

Without guards, you'd need separate states for every condition:

┌───────────────────┐  withdraw   ┌───────────────────┐
│  SUFFICIENT_FUNDS │ ──────────► │    DISPENSING     │
└───────────────────┘             └───────────────────┘

┌───────────────────┐  withdraw   ┌───────────────────┐
│ INSUFFICIENT_FUNDS│ ──────────► │   SHOWING_ERROR   │
└───────────────────┘             └───────────────────┘

Now multiply this by every other condition (card valid? daily limit reached? machine has cash?) and your state space explodes.

The Solution: Guards on Transitions

Guards let you keep a simple state structure while expressing conditional logic on the transitions themselves.

                        withdraw [balance >= amount]
                    ┌─────────────────────────────────────┐
                    │                                     ▼
              ┌───────────┐                        ┌───────────┐
              │   IDLE    │                        │ DISPENSING│
              └───────────┘                        └───────────┘
                    │
                    │ withdraw [balance < amount]
                    ▼
              ┌───────────┐
              │   ERROR   │
              └───────────┘

The same event (withdraw) can lead to different states depending on the guard condition. The state machine remains simple; the business logic lives in the guards.

Syntax note: Guards are typically written in square brackets after the event name: event [condition].

Benefits of guards:

  • Keep state count low. You don't need separate states for every precondition.
  • Business logic stays visible. Guards are declared alongside transitions, not buried in handlers.
  • Combine freely with other features. Guards work with hierarchies, parallel states, and everything else.

Parallel States: Independent Concurrent Behavior

Real systems often have multiple aspects that operate independently. A smartphone can be playing music while also connected to WiFi while also showing notifications. These aren't sequential states—they're simultaneous.

The Problem: Combinatorial State Explosion

Imagine modeling a simple media player with two independent concerns:

  1. Playback: playing or paused
  2. Volume: muted or audible

Without parallel states, you need to enumerate every combination:

┌─────────────────────┐
│  PLAYING_AUDIBLE    │
└─────────────────────┘
┌─────────────────────┐
│  PLAYING_MUTED      │
└─────────────────────┘
┌─────────────────────┐
│  PAUSED_AUDIBLE     │
└─────────────────────┘
┌─────────────────────┐
│  PAUSED_MUTED       │
└─────────────────────┘

That's 2 × 2 = 4 states. Now add a third concern—connectivity: online or offline. Now you have 2 × 2 × 2 = 8 states. Add a fourth concern and you have 16. Each new independent aspect doubles your state count.

The Solution: Parallel Regions

Parallel states let you model independent concerns in separate regions that execute simultaneously. The system is in one state from each region at the same time.

┌─────────────────────────────────────────────────────────────┐
│                       MEDIA_PLAYER                          │
│  ┌─────────────────────────┐  ┌─────────────────────────┐   │
│  │        Playback         │  │         Volume          │   │
│  │  ┌─────────┐ ┌────────┐ │  │  ┌────────┐ ┌─────────┐ │   │
│  │  │ PLAYING │ │ PAUSED │ │  │  │ MUTED  │ │ AUDIBLE │ │   │
│  │  └─────────┘ └────────┘ │  │  └────────┘ └─────────┘ │   │
│  │       ▲          │      │  │       ▲          │      │   │
│  │       └── play ──┘      │  │       └── mute ──┘      │   │
│  │       ┌── pause ─┘      │  │       ┌── mute ──┘      │   │
│  │       ▼                 │  │       ▼                 │   │
│  └─────────────────────────┘  └─────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

The current state is now a tuple: (playing, muted) or (paused, audible), etc. But you only define 2 + 2 = 4 states, not 2 × 2 = 4 compound states. The savings grow dramatically with complexity.

With three concerns:

  • Flat: 2 × 2 × 2 = 8 states to define
  • Parallel: 2 + 2 + 2 = 6 states to define

With five concerns:

  • Flat: 2⁵ = 32 states to define
  • Parallel: 2 × 5 = 10 states to define

Benefits of parallel states:

  • Eliminate combinatorial explosion. State count grows additively, not multiplicatively.
  • Model reality accurately. Independent subsystems really are independent. Your model should reflect that.
  • Isolate changes. Modifying the volume logic doesn't touch the playback logic.
  • Each region can have its own hierarchy. You can nest state machines inside parallel regions for arbitrary complexity.

History States: Remembering Where You Were

When you exit a parent state and later return, which substate should become active? By default, state machines re-enter at a defined initial state. But sometimes you want to resume where you left off.

The Problem: Losing Context on Exit

Picture the media player from before. You're in the ON state, happily PLAYING music. The phone rings—an incoming_call event takes you to a CALL state. When the call ends, you return to ON.

┌─────────────────────────────────────────────────┐
│                      ON                         │
│  ┌───────────┐    ┌───────────┐    ┌─────────┐  │
│  │  PLAYING  │    │  PAUSED   │    │ STOPPED │  │
│  └───────────┘    └───────────┘    └─────────┘  │
│                                                 │
└───────────────────────┬─────────────────────────┘
                        │
            incoming_call / end_call
                        │
                        ▼
                  ┌───────────┐
                  │   CALL    │
                  └───────────┘

Without history, returning to ON means starting at the initial substate (maybe STOPPED). Your music doesn't resume—you have to manually start it again. That's a poor user experience.

The Solution: History States

A history state (denoted H) is a pseudo-state that remembers the last active substate before exit. When you transition to the history state, you resume where you left off.

┌─────────────────────────────────────────────────┐
│                      ON                         │
│                                                 │
│  ┌───┐                                          │
│  │ H │◄──────────────────────────────┐          │
│  └───┘                               │          │
│    │ (resumes last active state)     │          │
│    ▼                                 │          │
│  ┌───────────┐    ┌───────────┐    ┌─────────┐  │
│  │  PLAYING  │◄──►│  PAUSED   │◄──►│ STOPPED │  │
│  └───────────┘    └───────────┘    └─────────┘  │
│                                                 │
└─────────────────────────┬───────────────────────┘
                          │ incoming_call
                          ▼
                    ┌───────────┐
                    │   CALL    │
                    └───────────┘
                          │ end_call → go to H
                          └──────────────────────►

Now when end_call fires, the transition targets H inside ON. If you were PLAYING, you return to PLAYING. If you were PAUSED, you return to PAUSED. Context is preserved.

Shallow vs. Deep History

There are two types of history:

Shallow history (H) remembers only the immediate child state. If that child itself has substates, it enters at its initial state.

Deep history (H)* remembers the full nested state configuration, no matter how deep. It restores exactly where you were, substates and all.

┌────────────────────────────────────────────────────────────┐
│                           ON                               │
│  ┌───┐  ┌────┐                                             │
│  │ H │  │ H* │                                             │
│  └───┘  └────┘                                             │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                      PLAYING                         │  │
│  │  ┌──────────┐    ┌──────────┐    ┌────────────────┐  │  │
│  │  │  NORMAL  │    │ FAST_FWD │    │ BUFFERING      │  │  │
│  │  └──────────┘    └──────────┘    └────────────────┘  │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                            │
│  ┌───────────┐    ┌───────────┐                            │
│  │  PAUSED   │    │  STOPPED  │                            │
│  └───────────┘    └───────────┘                            │
└────────────────────────────────────────────────────────────┘

If you were in PLAYING → BUFFERING when the call came in:

  • Shallow history (H) returns to PLAYING, then enters its initial substate (probably NORMAL).
  • Deep history (H)* returns directly to PLAYING → BUFFERING.

Benefits of history states:

  • Preserve user context. Interruptions don't lose progress.
  • Simplify re-entry logic. No need to manually track "where was I?" in external variables.
  • Work with hierarchies. History is per-parent-state, so different parts of your machine can have independent memory.
  • Choose your granularity. Shallow history for coarse recovery, deep history for exact restoration.

Putting It All Together

These features—hierarchy, guards, parallel states, and history—aren't isolated tricks. They compose. A real-world statechart might have:

  • Parallel regions for independent subsystems
  • Hierarchy within each region for organized complexity
  • Guards on transitions to express business rules
  • History states to preserve context across interruptions

This is the power of statecharts: a small set of well-designed primitives that combine to model arbitrarily complex behavior while remaining visualizable, analyzable, and maintainable.


SCXML: State Machines in Practice

Now that you understand the concepts, let's see them in action. SCXML (State Chart XML) is a W3C standard that lets you define state machines in XML format. The following interactive examples demonstrate each concept with real, runnable code.


Example 1: Turnstile (Basic States & Transitions)

The classic turnstile: insert a coin to unlock, push through to lock again.

Try It

The SCXML

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="null" initial="locked" name="Turnstile">

    <state id="locked">
        <onentry>
            <log label="Turnstile" expr="'LOCKED - Insert coin to enter'"/>
        </onentry>
        <transition event="coin" target="unlocked"/>
        <!-- Pushing while locked does nothing useful -->
        <transition event="push" target="locked">
            <log label="Turnstile" expr="'Still locked! Insert a coin first.'"/>
        </transition>
    </state>

    <state id="unlocked">
        <onentry>
            <log label="Turnstile" expr="'UNLOCKED - Push to enter'"/>
        </onentry>
        <transition event="push" target="locked"/>
        <!-- Extra coins while unlocked are just collected -->
        <transition event="coin" target="unlocked">
            <log label="Turnstile" expr="'Thanks for the extra coin!'"/>
        </transition>
    </state>

</scxml>

Key Concepts Demonstrated

  • Two states: locked and unlocked
  • Events trigger transitions: coin unlocks, push locks
  • Self-transitions: Handling events that don't change state (extra coins, pushing when locked)
  • Entry actions: <onentry> logs the current state

Example 2: Simple Toggle (Minimal State Machine)

The simplest possible state machine that actually toggles between two states: on and off.

Try It

The SCXML

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="null" initial="off" name="Toggle">

    <state id="off">
        <onentry>
            <log label="State" expr="'OFF - Light is off'"/>
        </onentry>
        <transition event="toggle" target="on"/>
    </state>

    <state id="on">
        <onentry>
            <log label="State" expr="'ON - Light is on'"/>
        </onentry>
        <transition event="toggle" target="off"/>
    </state>

</scxml>

Key Concepts Demonstrated

  • Two states: off and on
  • Single event: toggle switches between states
  • Entry actions: <onentry> logs when entering each state
  • Null datamodel: No variables needed for this simple example

Example 3: Door with Lock (Guards/Conditions)

A door that can be open, closed, or locked. Locking only works when closed.

Try It

The SCXML

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="ecmascript" initial="closed" name="Door">

    <datamodel>
        <data id="lockAttempts" expr="0"/>
    </datamodel>

    <state id="open">
        <onentry>
            <log label="Door" expr="'OPEN - Door is open'"/>
        </onentry>
        <transition event="close" target="closed"/>
        <!-- Cannot lock an open door -->
        <transition event="lock" target="open">
            <log label="Error" expr="'Cannot lock an open door!'"/>
        </transition>
    </state>

    <state id="closed">
        <onentry>
            <log label="Door" expr="'CLOSED - Door is closed'"/>
        </onentry>
        <transition event="open" target="open"/>
        <transition event="lock" target="locked">
            <assign location="lockAttempts" expr="lockAttempts + 1"/>
            <log label="Info" expr="'Locked (attempt #' + lockAttempts + ')'"/>
        </transition>
    </state>

    <state id="locked">
        <onentry>
            <log label="Door" expr="'LOCKED - Door is locked'"/>
        </onentry>
        <transition event="unlock" target="closed"/>
        <!-- Cannot open a locked door -->
        <transition event="open" target="locked">
            <log label="Error" expr="'Cannot open a locked door!'"/>
        </transition>
    </state>

</scxml>

Key Concepts Demonstrated

  • Three states: open, closed, locked
  • Constrained transitions: Can only lock when closed
  • Variables: lockAttempts counter using ECMAScript datamodel
  • Transition actions: <assign> modifies variables
  • Self-transitions: Staying in same state while logging errors

Example 4: Media Player (Hierarchical States)

A media player with nested states. The "playing" state contains sub-states for playback speed.

Try It

The SCXML

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="ecmascript" initial="stopped" name="MediaPlayer">

    <datamodel>
        <data id="position" expr="0"/>
        <data id="duration" expr="100"/>
    </datamodel>

    <state id="stopped">
        <onentry>
            <log label="Player" expr="'STOPPED'"/>
            <assign location="position" expr="0"/>
        </onentry>
        <transition event="play" target="playing"/>
    </state>

    <!-- Compound state: playing contains sub-states -->
    <state id="playing" initial="normal">
        <onentry>
            <log label="Player" expr="'PLAYING at position ' + position"/>
        </onentry>

        <!-- Transition from parent applies to all children -->
        <transition event="stop" target="stopped"/>
        <transition event="pause" target="paused"/>

        <!-- Normal playback speed -->
        <state id="normal">
            <onentry>
                <log label="Speed" expr="'Normal speed (1x)'"/>
            </onentry>
            <transition event="fastforward" target="fast"/>
            <transition event="rewind" target="rewinding"/>
        </state>

        <!-- Fast forward (2x speed) -->
        <state id="fast">
            <onentry>
                <log label="Speed" expr="'Fast forward (2x)'"/>
            </onentry>
            <transition event="normal" target="normal"/>
            <transition event="rewind" target="rewinding"/>
        </state>

        <!-- Rewind (-1x speed) -->
        <state id="rewinding">
            <onentry>
                <log label="Speed" expr="'Rewinding (-1x)'"/>
            </onentry>
            <transition event="normal" target="normal"/>
            <transition event="fastforward" target="fast"/>
        </state>
    </state>

    <state id="paused">
        <onentry>
            <log label="Player" expr="'PAUSED at position ' + position"/>
        </onentry>
        <transition event="play" target="playing"/>
        <transition event="stop" target="stopped"/>
    </state>

</scxml>

Key Concepts Demonstrated

  • Hierarchical states: playing contains normal, fast, rewinding
  • Initial child state: playing starts in normal sub-state
  • Parent transitions: stop and pause work from any child state
  • State refinement: Playing behavior is further specified by speed sub-state

Why Hierarchical States?

Without hierarchy, you'd need separate states for every combination:

  • playing_normal, playing_fast, playing_rewinding
  • paused_was_normal, paused_was_fast, paused_was_rewinding

Hierarchy eliminates this explosion by grouping related states.


Example 5: Crosswalk Signal (Parallel States)

A pedestrian crosswalk with two independent concerns running simultaneously:

  1. The walk/don't-walk signal
  2. The countdown timer display

Try It

The SCXML

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="ecmascript" initial="active" name="Crosswalk">

    <datamodel>
        <data id="countdown" expr="30"/>
    </datamodel>

    <!-- Parallel state: both regions active simultaneously -->
    <parallel id="active">
        <onentry>
            <log label="Crosswalk" expr="'Signal activated'"/>
        </onentry>

        <!-- Region 1: Walk signal -->
        <state id="signal" initial="dont_walk">
            <state id="dont_walk">
                <onentry>
                    <log label="Signal" expr="'DON\\'T WALK - Hand symbol'"/>
                </onentry>
                <transition event="button_pressed" target="walk"/>
            </state>

            <state id="walk">
                <onentry>
                    <log label="Signal" expr="'WALK - Walking symbol'"/>
                    <assign location="countdown" expr="30"/>
                    <send event="start_countdown"/>
                </onentry>
                <transition event="countdown_done" target="flashing"/>
            </state>

            <state id="flashing">
                <onentry>
                    <log label="Signal" expr="'FLASHING - Hurry up!'"/>
                    <send event="flash_done" delay="5s"/>
                </onentry>
                <transition event="flash_done" target="dont_walk"/>
            </state>
        </state>

        <!-- Region 2: Countdown display (runs in parallel) -->
        <state id="display" initial="hidden">
            <state id="hidden">
                <onentry>
                    <log label="Display" expr="'Countdown hidden'"/>
                </onentry>
                <transition event="start_countdown" target="counting"/>
            </state>

            <state id="counting">
                <onentry>
                    <log label="Display" expr="'Countdown: ' + countdown + ' seconds'"/>
                    <send event="tick" delay="1s"/>
                </onentry>
                <transition event="tick" cond="countdown > 0" target="counting">
                    <assign location="countdown" expr="countdown - 1"/>
                </transition>
                <transition event="tick" cond="countdown == 0" target="hidden">
                    <send event="countdown_done"/>
                </transition>
            </state>
        </state>
    </parallel>

</scxml>

Key Concepts Demonstrated

  • Parallel states: <parallel> with two independent regions
  • Simultaneous activity: Signal and display states are both active
  • Cross-region communication: start_countdown event affects both regions
  • Delayed events: <send delay="1s"> for countdown ticks
  • Conditional transitions: cond="countdown > 0" guards

Why Parallel States?

Parallel states model independent concerns that evolve simultaneously. Without them, you'd need to enumerate every combination:

  • dont_walk_hidden, dont_walk_counting
  • walk_hidden, walk_counting
  • flashing_hidden, flashing_counting

With parallelism, each concern is modeled separately and cleanly.


Example 6: Form Wizard (History States)

A multi-step form wizard that remembers which step you were on when you leave and return.

Try It

The SCXML

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="ecmascript" initial="idle" name="FormWizard">

    <datamodel>
        <data id="formData" expr="{}"/>
    </datamodel>

    <state id="idle">
        <onentry>
            <log label="Wizard" expr="'Ready to start form'"/>
        </onentry>
        <transition event="start" target="form"/>
    </state>

    <state id="form" initial="step1">
        <onentry>
            <log label="Wizard" expr="'Form in progress'"/>
        </onentry>

        <!-- History state remembers which step we were on -->
        <history id="form_history" type="shallow">
            <transition target="step1"/>
        </history>

        <transition event="cancel" target="idle"/>
        <transition event="save_draft" target="draft_saved"/>

        <state id="step1">
            <onentry>
                <log label="Step" expr="'Step 1: Personal Information'"/>
            </onentry>
            <transition event="next" target="step2"/>
        </state>

        <state id="step2">
            <onentry>
                <log label="Step" expr="'Step 2: Contact Details'"/>
            </onentry>
            <transition event="back" target="step1"/>
            <transition event="next" target="step3"/>
        </state>

        <state id="step3">
            <onentry>
                <log label="Step" expr="'Step 3: Review & Submit'"/>
            </onentry>
            <transition event="back" target="step2"/>
            <transition event="submit" target="submitted"/>
        </state>
    </state>

    <state id="draft_saved">
        <onentry>
            <log label="Wizard" expr="'Draft saved! You can resume later.'"/>
        </onentry>
        <!-- Resume returns to history - remembers which step -->
        <transition event="resume" target="form_history"/>
        <transition event="discard" target="idle"/>
    </state>

    <state id="submitted">
        <onentry>
            <log label="Wizard" expr="'Form submitted successfully!'"/>
        </onentry>
        <transition event="new_form" target="idle"/>
    </state>

</scxml>

Key Concepts Demonstrated

  • History states: <history> remembers the last active child state
  • Shallow vs deep history: Shallow remembers immediate children only
  • Resume functionality: Returning to form_history restores previous step
  • Default history transition: If no history, defaults to step1

When to Use History

History states are useful when:

  • Users may leave and return to a complex flow
  • Interruptions should be resumable (save draft, timeout, etc.)
  • Modal dialogs should return to previous context

Example 7: Authenticated Session (Guards with Data)

A session manager demonstrating guards with data conditions and token refresh.

Try It

The SCXML

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="ecmascript" initial="logged_out" name="Session">

    <datamodel>
        <data id="username" expr="''"/>
        <data id="loginAttempts" expr="0"/>
        <data id="maxAttempts" expr="3"/>
        <data id="tokenExpiry" expr="0"/>
    </datamodel>

    <state id="logged_out">
        <onentry>
            <log label="Session" expr="'Logged out'"/>
            <assign location="username" expr="''"/>
            <assign location="loginAttempts" expr="0"/>
        </onentry>
        <transition event="login" target="authenticating"/>
    </state>

    <state id="authenticating">
        <onentry>
            <log label="Session" expr="'Authenticating...'"/>
            <assign location="loginAttempts" expr="loginAttempts + 1"/>
        </onentry>

        <!-- Success: go to logged_in -->
        <transition event="auth_success" target="logged_in">
            <assign location="username" expr="_event.data.username"/>
            <log label="Session" expr="'Welcome, ' + _event.data.username"/>
        </transition>

        <!-- Failure with retries remaining -->
        <transition event="auth_failure" cond="loginAttempts &lt; maxAttempts" target="logged_out">
            <log label="Session" expr="'Login failed. Attempts: ' + loginAttempts + '/' + maxAttempts"/>
        </transition>

        <!-- Failure with no retries left -->
        <transition event="auth_failure" cond="loginAttempts >= maxAttempts" target="locked">
            <log label="Session" expr="'Too many failed attempts. Account locked.'"/>
        </transition>
    </state>

    <state id="logged_in">
        <onentry>
            <log label="Session" expr="'Logged in as: ' + username"/>
        </onentry>
        <transition event="logout" target="logged_out"/>
        <transition event="token_expired" target="refreshing"/>
    </state>

    <state id="refreshing">
        <onentry>
            <log label="Session" expr="'Refreshing token...'"/>
        </onentry>
        <transition event="refresh_success" target="logged_in">
            <log label="Session" expr="'Token refreshed successfully'"/>
        </transition>
        <transition event="refresh_failure" target="logged_out">
            <log label="Session" expr="'Token refresh failed. Please log in again.'"/>
        </transition>
    </state>

    <state id="locked">
        <onentry>
            <log label="Session" expr="'Account locked for security'"/>
        </onentry>
        <transition event="unlock" target="logged_out">
            <log label="Session" expr="'Account unlocked by administrator'"/>
        </transition>
    </state>

</scxml>

Key Concepts Demonstrated

  • Guard conditions: cond="loginAttempts < maxAttempts" controls flow
  • Event data: _event.data.username accesses event payload
  • Multiple guarded transitions: Same event, different guards, different targets
  • Complex data operations: Counters, string data

Example 8: Phone Call (Complete Hierarchical Example)

A complete phone call state machine demonstrating hierarchical states, timers, and real-world complexity.

Try It

Key Concepts Demonstrated

  • Complex hierarchy: Active state contains DialTone, Dialing, Connecting, Ringing, Talking, and error states
  • Timed transitions: <send delay="15s"> for dial tone timeout
  • Timer cancellation: <cancel sendid="timeout_timer"/> in <onexit>
  • Guard conditions: cond="digitCount < targetLength - 1" for digit counting
  • Self-transitions: Staying in Dialing while accumulating digits
  • Multiple exit paths: Timeout, invalid number, busy signal, successful connection
  • Final state: ExitPoint for service cancellation

Why This Example?

The phone call demonstrates how state machines elegantly handle:

  • Timeouts: Automatic reset after 15 seconds of inactivity
  • Complex flows: Multiple paths through dialing, connecting, talking
  • Error handling: Invalid numbers, busy signals, timeouts
  • Resource cleanup: Timer cancellation on state exit

SCXML Feature Summary

Feature Element Description
States <state> Basic state container
Parallel <parallel> Concurrent regions
Final <final> Completion state
Initial initial attr Starting state
Transitions <transition> State changes
Events event attr Transition triggers
Guards cond attr Conditional transitions
Entry Actions <onentry> Run when entering state
Exit Actions <onexit> Run when leaving state
Assign <assign> Modify variables
Log <log> Debug output
Send <send> Queue events (with optional delay)
Cancel <cancel> Cancel delayed events
Raise <raise> Queue internal events
If/Else <if>, <else> Conditional actions
Foreach <foreach> Loop over arrays
Script <script> Arbitrary code
Datamodel <datamodel> Variable declarations
History <history> Remember previous state
Invoke <invoke> Spawn child machines

Further Reading

Standards & Specifications

Conceptual Background

Tutorials & Guides


Next Steps

Ready to build your own state machines?

  1. SCXML Reference - Complete language reference
  2. Visual Editor - Design statecharts visually
  3. Simulator - Test and debug interactively
  4. Code Generator - Generate Java, JavaScript, or C code