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)
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
isLoadingandisSuccessfrom both beingtrue. 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)
// 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 && isSuccessbug 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
transitionsobject 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:
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:
- Design your state machine visually in the editor
- Test it interactively in the simulator until the behavior is correct
- Generate production code for your target platform
- 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
ONstate 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:
- Playback:
playingorpaused - Volume:
mutedoraudible
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 (probablyNORMAL). - 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 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:
lockedandunlocked - Events trigger transitions:
coinunlocks,pushlocks - 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 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:
offandon - Single event:
toggleswitches 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 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:
lockAttemptscounter 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 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:
playingcontainsnormal,fast,rewinding - Initial child state:
playingstarts innormalsub-state - Parent transitions:
stopandpausework 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_rewindingpaused_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:
- The walk/don't-walk signal
- The countdown timer display
Try It
The SCXML
<?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_countdownevent 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_countingwalk_hidden,walk_countingflashing_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 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_historyrestores 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 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 < 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.usernameaccesses 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:
Activestate containsDialTone,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
Dialingwhile accumulating digits - Multiple exit paths: Timeout, invalid number, busy signal, successful connection
- Final state:
ExitPointfor 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
- W3C SCXML Specification - The official standard
- W3C State Chart XML - Final recommendation
Conceptual Background
- Statecharts: A Visual Formalism for Complex Systems - David Harel's original 1987 paper
- UML State Machine - Wikipedia overview
- Finite-state machine - General FSM concepts
Tutorials & Guides
- Statecharts.dev - Comprehensive statechart resource
- XState Documentation - Popular JavaScript implementation (concepts applicable to SCXML)
Next Steps
Ready to build your own state machines?
- SCXML Reference - Complete language reference
- Visual Editor - Design statecharts visually
- Simulator - Test and debug interactively
- Code Generator - Generate Java, JavaScript, or C code