Machina.js: Focused Finite State Machines for TypeScript
ts ex machina - finite state machines in TypeScript
At a glance
- What is it?
- Machina.js is a TypeScript-first finite state machine library for explicit transitions, hierarchical delegation, deferred inputs, and per-client state tracking. It suits applications that need state management without a broader statechart or actor framework.
- Who is it for?
- Adopt Machina.js when the requirement is a focused finite state machine, especially for TypeScript projects that benefit from inferred states and per-client behavior. Do not choose it when actors, spawning, or SCXML compatibility are core requirements.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Star history
- ifandelse/machina.js1,976 ★ · +0
| Repository | 2026-10-16 | 2026-10-24 |
|---|---|---|
| ifandelse/machina.js | 2k | 2k |
0 Stars in the last 8 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Machina.js state machines and their intended users
Machina.js addresses the need to model a process whose valid inputs and transitions are easier to express explicitly than with scattered flags and conditional statements. The library is aimed at JavaScript and TypeScript developers who want finite state machines, or a hierarchy of finite state machines, without adopting a statechart framework. Its README describes the design as focused and says that handlers receive inputs and return state names. That makes it relevant to device locks, connection lifecycles, queues, shopping flows, and other domains where the current state affects what can happen next.
The core choice is between two APIs. `createFsm` creates a machine that owns one context and represents one instance. `createBehavioralFsm` defines behavior once, then tracks independent state for multiple client objects in a `WeakMap`. The latter fits cases such as many connection objects sharing the same connection rules. The distinction matters because using `createFsm` for each connection would duplicate machine definitions, while using `createBehavioralFsm` with a single shared object would incorrectly share state between clients.
Machina also keeps the scope narrower than a full actor system. The README positions XState as the alternative when actors, spawning, inspection tools, or SCXML compatibility are needed. Machina therefore appears more appropriate for codebases that need a state transition model but do not need those broader abstractions.
How Machina.js routes inputs through states
A machine definition contains an `initialState`, a `context`, and a `states` map. Each state defines handlers or string shorthand entries. The caller sends an input with `handle()`. The handler receives the machine context as an argument when it needs it, and can return another state name to request a transition. Returning nothing keeps the machine in its current state. This rule makes transitions visible in the handler body rather than hiding them behind a larger event-routing abstraction.
For example, the locked state handles `submit` with a code. It increments `ctx.attempts`, returns `"unlocked"` when the code is `"1234"`, and returns `"alarming"` after three failed attempts. The unlocked state uses the shorthand form `lock: "locked"`, while the alarming state handles `reset` by clearing attempts and returning to locked. The mechanism is direct: input name selects a handler in the active state, handler logic runs, and an optional returned state name controls the next state.
Machina also supports `defer()`, which re-queues an input for replay after the next transition. A handler can target a state with `defer({ until: "someStateName" })`. Hierarchical states can nest child machines through `_child`. Inputs delegate to the child first and bubble upward when the child has no handler. The README says hierarchical delegation can reach any depth in v7, while `bubbles` provides typed declarations for inputs sent upward to a mounting machine. These features make hierarchy more than a visual grouping, but they also require the parent-child routing rules to be understood before large configurations are introduced.
Installing Machina.js and running a door lock example
npm install machina
# or
pnpm add machinaDefining and exercising the first Machina.js FSM
import { createFsm } from "machina";
const lock = createFsm({
id: "door-lock",
initialState: "locked",
context: { attempts: 0 },
states: {
locked: {
submit({ ctx }, code: unknown) {
ctx.attempts++;
if (code === "1234") {
return "unlocked";
}
if (ctx.attempts >= 3) {
return "alarming";
}
},
},
unlocked: {
lock: "locked",
},
alarming: {
reset({ ctx }) {
ctx.attempts = 0;
return "locked";
},
},
},
});Sending inputs and checking the current state
lock.handle("submit", "wrong");
lock.handle("submit", "1234");
lock.handle("lock");
console.log(lock.currentState());Per-client behavior with createBehavioralFsm
import { createBehavioralFsm } from "machina";
interface Connection {
url: string;
retries: number;
}
const connFsm = createBehavioralFsm<Connection>()({
id: "connectivity",
initialState: "disconnected",
states: {
disconnected: { connect: "connecting" },
connecting: {
connected: "online",
failed({ ctx }) {
ctx.retries++;
return "disconnected";
},
},
online: { disconnect: "disconnected" },
},
});Type safety, hierarchy, testing, and disposal boundaries
Machina's TypeScript support is a central part of its value, not just a wrapper around JavaScript objects. The README says state names, input names, and transition targets are inferred, and that string-shorthand typos become compile errors. Typed input payloads are opt-in through a payload map, such as `createFsm<TInputs>()({...})`. The declaration is reused for handler parameters and `handle()` calls, while undeclared handler keys become compile errors. This can catch configuration errors before runtime, although it does not replace tests for handler behavior.
The repository also contains companion packages. `machina-inspect` parses FSM configurations into a directed graph and checks for structural problems such as unreachable states, `_onEnter` loops, and missing handlers. `machina-test` supplies Jest and Vitest custom matchers for topology assertions. `eslint-plugin-machina` surfaces inspection findings in an editor, and `machina-explorer` provides a browser-based configuration analysis tool that renders a Mermaid diagram. These are separate packages or examples, not features automatically included by the core package.
Disposal has a strict boundary. `dispose()` is permanent and irreversible, cascades to child FSMs by default, and makes later calls silent no-ops. That is useful for teardown, but silent behavior can make accidental use after cleanup harder to notice. Applications should make disposal an explicit lifecycle step and decide whether default cascading matches their ownership model.
Machina.js limitations and the XState distinction
The clearest limitation is scope. Machina is a focused FSM library, while XState is described as a statechart framework with actors, spawning, inspection tools, and SCXML compatibility. If an application needs to spawn actors, coordinate long-running processes, or use an existing SCXML workflow representation, Machina does not document those capabilities. Choosing it for those requirements would mean building missing orchestration around the FSM.
Another boundary is configuration complexity. Hierarchical delegation, deferred input, lifecycle hooks, typed payload maps, and behavioral clients are all available, but they are related mechanisms rather than one feature. The parent and child machines must agree about delegation and bubbling. Deferred inputs require careful thought because an input can be processed again after a transition. The library reports built-in lifecycle events, including `transitioning`, `transitioned`, `handling`, `handled`, `nohandler`, and `deferred`, but the README does not provide a separate debugging interface for every possible application-level issue.
There is also no GitHub release history listed for the repository. The README identifies v7 as current and discusses v8 features in its overview, while the repository metadata has no GitHub releases. That makes the documentation and package version context important when selecting a version. The repository is not archived, and its last push was on 2026-10-05, but release policy and upgrade compatibility should be checked directly rather than inferred from commit activity. The custom `LICENSE` file also needs review before adoption, especially when the package is embedded in a commercial distribution.
Monorepo scope, package boundaries, and operational use
The repository is a monorepo. The core library is under `packages/machina`, with additional packages for inspection, testing, and ESLint integration. Examples cover connectivity, queues, shopping carts, traffic intersections, testing, React integration, and inspection. This layout gives maintainers places to exercise different configurations, but consumers should distinguish the core `machina` package from companion tools. Installing a companion package does not automatically install every package in the workspace.
The core README provides lifecycle events and permanent disposal, but it does not document a production deployment configuration, a persistence layer, a transport protocol, or an application-specific migration runner. Machina manages state transitions and context. It does not, based on the available documentation, provide durable storage for a process or a network client implementation. The connectivity example shows how to model connection states, not how to establish a WebSocket. Likewise, the job queue example is evidence of an example area in the repository, not documentation that the core package implements a queue.
This boundary is useful when evaluating adoption. A team can place Machina beside domain services, UI components, or transport code and use it as the explicit rule layer for valid state changes. It is less suitable as a complete application runtime. The package's disposal behavior also implies that integration code needs to define ownership and teardown rather than assuming a machine can be restarted after disposal.
Editorial conclusion
Adopt Machina.js when the requirement is a focused finite state machine, especially for TypeScript projects that benefit from inferred states and per-client behavior. Do not choose it when actors, spawning, or SCXML compatibility are core requirements. Verify the custom LICENSE file and test typed payloads, hierarchy, and disposal behavior against the application first.
Frequently asked questions
How do I install Machina.js in a TypeScript project?
The README documents `npm install machina` and `pnpm add machina`. The project provides TypeScript-first APIs, but the README does not give a separate TypeScript compiler installation command.
What is the difference between createFsm and createBehavioralFsm in Machina.js?
`createFsm` creates a single-client machine that owns its context. `createBehavioralFsm` defines shared behavior and tracks state per client object in a `WeakMap`, allowing multiple independent clients to use the same definition.
How do handlers change the state of a Machina.js FSM?
A handler returns a state name to transition, or returns nothing to remain in the current state. Simple transitions can also use string shorthand, such as `lock: "locked"`.
Does Machina.js support actors, spawning, or SCXML compatibility?
The README identifies XState as the option for actors, spawning, inspection tools, and SCXML compatibility. Machina.js is presented as a focused finite state machine library and does not document those features in the core API.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ifandelse-machina-js)Star history chart for your README
[](https://hysenlabs.com/projects/ifandelse-machina-js)Was this useful?