jssm 5.163.6
Try the live editor ·
Documentation ·
Discord ·
Issues
Easy. Tiny. Fast. Finite state machines as one-liner strings, for
TypeScript and JavaScript. Renders to PNG, SVG, and JPEG. Runs in Node,
browsers, and Deno. MIT licensed.
import { sm } from 'jssm';
const TrafficLight = sm`Red -> Green -> Yellow -> Red;`;
That’s it. Using it is equally easy:
TrafficLight.state();
TrafficLight.go('Green');
TrafficLight.state();
The point of a state machine is to refuse to do things that aren’t correct:
TrafficLight.go('Red');
TrafficLight.go('Blue');
A more involved machine, with main paths, forced paths, and per-state
styling, renders to:
const TrafficLightWithOff = sm`
Red 'next' => Green 'next' => Yellow 'next' => Red;
[Red Yellow Green] ~> Off -> Red;
flow: left;
state Red : { background-color: pink; corners: rounded; };
state Yellow : { background-color: lightyellow; corners: rounded; };
state Green : { background-color: lightgreen; corners: rounded; };
state Off : {
background-color : steelblue;
text-color : white;
shape : octagon;
linestyle : dashed;
};
`;
The same string is the runtime and the diagram. They cannot drift apart.
Try it in the live editor ·
Documentation ·
Discord ·
Issues
Install
npm install jssm
The package ships pure ES6, a CommonJS ES5 bundle, an IIFE for browsers,
and TypeScript typings. A Deno build is included. Node 10 or newer.
Visualization
jssm ships with a visualization subpath that renders state machines to
SVG using Graphviz (via @viz-js/viz).
import { sm } from 'jssm';
import { fsl_to_svg_string } from 'jssm/viz';
const svg = await fsl_to_svg_string('a -> b;');
The viz subpath is opt-in - importing only from jssm does not pull in
@viz-js/viz. See the Visualization doc page for browser, ESM, and IIFE
usage patterns.
Command-line interface
jssm ships a CLI for rendering FSL machines to images and other formats.
Installation
npm install -g jssm
This installs three binaries: fsl (the dispatcher), jssm (alias for fsl), and fsl-render (the render plugin).
Render
Render a single machine to SVG (default):
fsl render machine.fsl
# → machine.svg next to input
Specify a format:
fsl render machine.fsl --target=png --width=800
fsl render machine.fsl --target=dot --stdout > machine.dot
Render multiple machines:
fsl render *.fsl --target=svg --out-dir=./diagrams
Pipe FSL via stdin:
cat machine.fsl | fsl render --target=dot | dot -Tpng > out.png
Plugin architecture
Every fsl-<name> executable on PATH is dispatched when you run fsl <name>. Third-party plugins follow the same contract as first-party fsl-render. See notes/superpowers/specs/2026-05-12-fsl-cli-design.md for the contract.
Library API
The same render functions are available programmatically:
import { render, renderSet } from 'jssm/cli';
const result = await render(fslText, { target: 'svg' });
if (result.kind === 'text') console.log(result.content);
Static fence rendering (jssm/fence)
jssm/fence renders FSL code fences to static, editor-parity-highlighted HTML — no browser, no editor, no live machine required — for Markdown pipelines, static site generators, and doc tooling.
import { transform_markdown } from 'jssm/fence';
const html = await transform_markdown(markdown);
// every fsl/jssm fence becomes rendered, highlighted HTML
// everything else in the document passes through untouched
console.log(html);
render_fence_gif walks a machine’s main path (or every edge, if there is no main path) into a looping animated GIF, one Graphviz layout patched per frame: 70cs per-frame delay, loops forever, and a 64-frame walk cap by default, all overridable via options.
PNG, JPEG, and GIF output rasterize through the same optional backend as the CLI — install it with npm install @resvg/resvg-wasm.
TextMate grammar (jssm/grammar)
jssm ships the canonical TextMate grammar for FSL as a data file at dist/grammars/fsl.tmLanguage.json (scope source.fsl), the single source consumed across the editor ecosystem — shiki (and the static-site generators built on it), GitHub Linguist, Monaco, Sublime, bat, and the vscode-fsl extension.
import grammar from 'jssm/grammar' with { type: 'json' };
The grammar is generated from — and build-verified against — the same fsl_parser.peg that drives the parser, so its highlighting never drifts from the language.
Web Components
jssm ships Lit-based web components for use in plain HTML or as a base for framework wrappers.
CDN one-liner (with an import map for @viz-js/viz):
<script type="module" src="https://cdn.jsdelivr.net/npm/jssm/dist/cdn/viz.js"></script>
<fsl-viz fsl="Off -> On -> Off;"></fsl-viz>
npm one-liner:
import 'jssm/wc/viz/define';
// then use <fsl-viz fsl="..."> anywhere; <jssm-viz> is an accepted alias
Full documentation: src/doc_md/WebComponents.md.
60-second tour
Actions let a machine advance without the caller knowing the next state:
const Light = sm`Red 'next' -> Green 'next' -> Yellow 'next' -> Red;`;
Light.action('next');
Light.state();
Light.action('next');
Light.state();
Three arrow types distinguish kinds of transition:
const Light = sm`
Red => Green => Yellow => Red; // => main path
[Red Yellow Green] ~> Off -> Red; // ~> forced, -> legal
`;
-> is a legal transition. => is a legal transition that is also part of
the main path. ~> is a transition that requires force_transition -
useful for emergency stops, resets, and other rarities.
Hooks observe and gate transitions:
const m = sm`Red 'next' -> Green 'next' -> Yellow 'next' -> Red;`
.hook('Red', 'Green', () => console.log('GO'))
.hook_entry('Red', () => console.log('STOP'))
.hook_action('Yellow', 'Red', 'next', () => allowed());
Pre-hooks fire before the state changes and may return false to refuse the
transition. Post-hooks fire after. Four *_everything hooks
(hook_pre_everything, hook_everything, hook_pre_post_everything,
hook_post_everything) bracket the entire pipeline if you want a single
observation point.
Refusals and errors are deliberately different. An illegal transition
returns false. An unknown state throws. Branching code can rely on the
distinction.
Overlapping state groups let a state belong to several groups at once -
something a strict hierarchy can’t express. A group is declared with &,
and the same &name then drives transitions, shared metadata, boundary
hooks, and runtime queries:
const req = sm`
&InProgress : [connecting sending receiving];
&Receiving : [receiving draining];
idle 'send' -> connecting 'open' -> sending 'reply' -> receiving;
receiving 'eof' -> draining 'done' -> idle;
&InProgress 'abort' -> idle; // a transition from every group member
on enter &Receiving do 'log_rx'; // boundary hook fires crossing in
`;
req.action('send');
req.isIn('InProgress');
req.groupsOf('receiving');
receiving is in both groups simultaneously - it is in-progress and
receiving. When two groups disagree about the same action, a CSS-like
cascade decides: state-specific edges win, then the innermost (nearest)
group, then the later-declared one. Groups render as nested Graphviz
clusters, or as bracketed chips on the node label where memberships
genuinely overlap.
See the cookbook’s overlapping-groups recipes for fuller worked examples.
Why jssm
The big win: most state-machine libraries make you write a gargantuan
JSON document, or call a builder API a few dozen times, to define a single
machine. jssm machines are short, readable, arrow-driven strings - so they
are easy to write, easy to read, easy to debug, and easy to share.
That decision shows up everywhere downstream:
-
A DSL with features other state-machine libraries don’t have. Three
arrow types distinguish legal, main-path, and forced transitions. Array
notation collapses repeated edges - [Red Yellow Green] ~> Off replaces
three lines. Named actions, per-state styling, named edges, validators,
and live visualization all live in the same string the runtime parses.
-
Definition strings stay tiny, which makes machines easy to debug.
The traffic light is one line; the full eight-step ATM walkthrough
is thirty. Small enough to read top-to-bottom, diff in code review,
paste into a bug report, or drop into the
live editor
for a rendered diagram you can step through.
-
Fast. Tens of millions of transitions per second on commodity
hardware. See src/buildjs/benchmark.cjs
or run npm run benny against your own machine.
-
More thoroughly tested than any other JavaScript state-machine
library. 10,142 tests at 100.0% line coverage
(report), plus
fuzz testing via fast-check, with parser test data across ten natural
languages and Emoji.
Documentation
- What are state machines? - conceptual intro for newcomers
- Getting started - install and use the library across Node, browser, Deno, ES5/ES6, CDN, and TypeScript
- Using jssm in the browser - script tags, CDN, native ES modules, bundlers, and web components
- Tutorial: a four-state traffic light - short walkthrough that introduces the three arrow types
- Tutorial: building an ATM state machine - longer walkthrough that builds a real-world machine in nine incremental steps
- Language reference - DSL reference for people already comfortable with state machines
- Styling nodes and graphs - the built-in named color themes and how to select them from FSL source, code, Markdown fences, the CLI, and web components
- Catalog of example machines - comparison table of worked examples (light switch, traffic light, intersection, vending machine, more)
- Generated API reference - full surface, generated from the TypeScript source
API at a glance
| Method |
Purpose |
sm`...` |
Build a machine from DSL |
.state() |
The current state |
.transition(state) |
Move to a state. Returns false if illegal, throws if unknown. |
.force_transition(state) |
Move to a state across a ~> forced edge |
.action(name) |
Trigger a named action. The next state is derived from the current state. |
.valid_transition(state) · .valid_action(name) |
Test whether a transition or action is legal from the current state, without taking it |
.hook(from, to, fn) |
Run on a specific edge. Pre-hook; return false to block. |
.hook_entry(state, fn) · .hook_exit(state, fn) |
Run when entering or leaving a state |
.hook_action(from, to, action, fn) |
Run when a named action causes a specific edge |
.hook_pre_everything(fn) · .hook_everything(fn) |
Bracket the pre-hook pipeline |
.hook_pre_post_everything(fn) · .hook_post_everything(fn) |
Bracket the post-hook pipeline |
The full surface - including history, validators, factories, data, and the
graph-introspection methods - is in the generated API
docs.
Status
In production use since May 2017. Current series is 5.x and the DSL is
stable; the runtime API has been additive for several years. MIT licensed
end to end. Test data and parser cases are included for English, German,
French, Spanish, Hebrew, Russian, Ukrainian, Belarusian, Bengali,
Portuguese, and Emoji.

Questions, design discussions, bug reports, and “what would you do for X?”
are all welcome on Discord. Issues that need a paper trail go in the
issue tracker.
Comparisons
A direct, head-to-head comparison with the other actively-maintained JS state
machine libraries - XState, Stately.js, Finity, machina.js, and others - is
in progress and will live in
FeatureComparison.md.
A list of related projects, without commentary, is at the bottom of that
file.
Contributing
Issues and PRs are welcome. The cheapest useful contribution is a language
test case: open a PR with
english.json
translated into your language. Translating
traffic_light.fsl
into a separate repo and publishing it goes a step further.
Acknowledgements
Michael Morgan has debated significant
sections of the notation, invented several concepts and operators, helped
with the parser and system nomenclature, and published the first non-author
FSL machine. Vat Raghavan participated
extensively in language design and implemented several features. Forest
Belton provided guidance, bugfixes, and
parser commentary. Jordan
Harbrand suggested two interesting features and
gave strong feedback on the initial tutorial draft.
Translation contributors:
If your contribution is missing here, please open an issue.
Stats, coverage, and badges
10,142 tests, run 99,539 times.
- 9,239 specs with 100.0% coverage
- 903 fuzz tests with 56.3% coverage
- 11,181 TypeScript lines - 0.9 tests per line, 8.9 generated tests per line

评论