# machina

> Focused finite state machines for JavaScript and TypeScript. States in, states out.

Latest version **7.0.1** (published 2026-08-11) · MIT license · 0 weekly downloads

## Install

```sh
npm install machina
pnpm add machina
yarn add machina
bun add machina
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 7.0.1 |
| Published | 2026-08-11 |
| First published | 2012-04-25 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=22.22 |
| Dependencies | 0 |
| Unpacked size | 461.3 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 1971 |
| Author | Jim Cowart |
| Maintainers | ifandelse, rniemeyer |

## Links

- npm: https://www.npmjs.com/package/machina
- Repository: https://github.com/ifandelse/machina.js
- Homepage: https://machina-js.org/
- Issues: http://github.com/ifandelse/machina.js/issues
- npm.io page: https://npm.io/package/machina

## Recent versions

- 7.0.1 (latest) — 2026-08-11
- 7.0.0 — 2026-08-11
- 6.3.0 — 2026-04-06
- 6.2.0 — 2026-02-28
- 6.1.0 — 2026-02-27
- 6.0.0 — 2026-02-23
- 4.0.2 — 2018-10-27
- 4.0.1 — 2018-10-25
- 4.0.0 — 2018-10-25
- 2.0.2 — 2018-02-16
- 2.0.1 — 2018-02-06
- 2.0.0 — 2016-06-25
- 2.0.0-1 — 2016-03-13
- 1.1.2 — 2015-06-21
- 1.1.1 — 2015-06-20
- … 23 more at https://npm.io/package/machina/versions

## README

# machina

Focused finite state machines for JavaScript and TypeScript. States in, states out.

## Full documentation

https://machina-js.org

## Install

```
npm install machina
```

## Quick start — `createFsm`

`createFsm` is the standard choice: one config, one FSM instance, one internal context object. Handler signatures receive a `{ ctx, inputName, defer, emit }` args object — no `this`, works with arrow functions.

Handlers return a state name to transition, or nothing to stay put.

```ts
import { createFsm } from "machina";

const light = createFsm({
    id: "traffic-light",
    initialState: "green",
    context: { tickCount: 0 },
    states: {
        green: {
            _onEnter({ ctx }) {
                ctx.tickCount = 0;
            },
            tick({ ctx }) {
                ctx.tickCount++;
            },
            timeout({ ctx }) {
                if (ctx.tickCount >= 5) return "yellow";
            },
        },
        yellow: {
            timeout: "red", // string shorthand — always transitions
        },
        red: {
            timeout: "green",
        },
    },
});

light.handle("tick");
light.handle("tick");
light.handle("timeout"); // stays green — tickCount is 2, not >= 5

light.currentState(); // "green"
light.compositeState(); // "green" (dot-delimited path, useful with child FSMs)
light.canHandle("timeout"); // true

light.reset(); // back to initialState, fires _onEnter
light.dispose(); // tears down; all subsequent calls are silent no-ops
```

### Public API — `Fsm`

| Method                       | Description                                                               |
| ---------------------------- | ------------------------------------------------------------------------- |
| `handle(inputName, ...args)` | Dispatch an input to the current state's handler                          |
| `canHandle(inputName)`       | True if the current state — or its `_child` chain — can handle this input |
| `transition(toState)`        | Directly transition; fires `_onExit`, `_onEnter`, lifecycle events        |
| `reset()`                    | Transition back to `initialState`                                         |
| `currentState()`             | Returns the current state name                                            |
| `compositeState()`           | Dot-delimited path including active child FSM states                      |
| `on(eventName, callback)`    | Subscribe to a lifecycle event (returns `{ off() }`)                      |
| `emit(eventName, data?)`     | Emit a custom event                                                       |
| `dispose(options?)`          | Permanently shut down; cascades to child FSMs by default                  |

## `createBehavioralFsm`

`createBehavioralFsm` defines behavior once and applies it to any number of independent client objects. Per-client state lives in a `WeakMap` — no properties are stamped onto the client. The client object IS the context; handlers receive it as `ctx`.

The client type can't be inferred from the config, so it's provided via a curried call — `createBehavioralFsm<Connection>()` — that fixes the client type before the config (and its literal state/input names) is inferred from the second call.

```ts
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++;
                if (ctx.retries >= 3) return "error";
                return "disconnected";
            },
        },
        online: {
            disconnect: "disconnected",
        },
        error: {
            reset({ ctx }) {
                ctx.retries = 0;
                return "disconnected";
            },
        },
    },
});

const connA = { url: "wss://host-a.example.com", retries: 0 };
const connB = { url: "wss://host-b.example.com", retries: 0 };

connFsm.handle(connA, "connect");
connFsm.handle(connB, "connect");
connFsm.handle(connB, "failed");

connFsm.currentState(connA); // "connecting"
connFsm.currentState(connB); // "disconnected"
```

`BehavioralFsm` has the same API as `Fsm`, except every method takes the client object as its first argument. It also adds `rehydrate(client, compositeState)` for restoring previously-serialized clients without triggering lifecycle hooks or events.

### Rehydrating persisted clients

`rehydrate()` places a client at a known state with no lifecycle activity — no `_onEnter`, no events. It accepts the same dot-path format that `compositeState()` produces, enabling cold-resume workflows where clients are serialized to external storage and later restored.

```ts
// Persist
const snapshot = { client: connA, state: connFsm.compositeState(connA) };

// Restore (possibly a different process)
const { client, state } = JSON.parse(stored);
connFsm.rehydrate(client, state); // silent — no _onEnter, no events
connFsm.handle(client, "retry"); // proceeds from restored state
```

For hierarchical FSMs, pass the dot-delimited path: `fsm.rehydrate(client, "active.uploading.retrying")`. Throws on invalid states or structural mismatches.

## Hierarchical states

Attach a child FSM to any state via `_child`. Inputs are dispatched to the child first; unhandled inputs bubble up to the parent.

```ts
import { createFsm } from "machina";

const childFsm = createFsm({
    id: "upload-phases",
    initialState: "preparing",
    context: {},
    states: {
        preparing: { ready: "uploading" },
        uploading: { done: "verifying" },
        verifying: { verified: "complete" },
        complete: {},
    },
});

const uploader = createFsm({
    id: "uploader",
    initialState: "idle",
    context: {},
    states: {
        idle: {
            start: "active",
        },
        active: {
            _child: childFsm, // delegates inputs here first
            cancel: "idle", // "cancel" is not on childFsm, so it bubbles up here
        },
    },
});

uploader.handle("start");
uploader.compositeState(); // "active.preparing"

uploader.handle("ready");
uploader.compositeState(); // "active.uploading"

uploader.handle("cancel");
uploader.compositeState(); // "idle"
```

The parent re-enters `initialState` of the child FSM whenever the parent transitions into the state that owns `_child`.

Delegation reaches any depth: `canHandle()` answers for the whole `_child` chain, so an input handled three levels down can be dispatched from the root. When both a descendant and an ancestor could handle an input, the descendant wins.

An FSM can also declare inputs it fires at itself but never handles — expecting whatever mounts it to catch them via bubbling:

```ts
const phase = createFsm({
    id: "phase",
    initialState: "running",
    context: {},
    bubbles: ["phaseComplete"], // "I fire this; whoever mounts me must deal with it"
    states: {
        running: {
            _onEnter() {
                // ...later: phase.handle("phaseComplete") — typed, no handler here
            },
        },
    },
});
```

Declared bubbles join the FSM's typed input union, and they form a compile-time contract: a config that mounts `phase` via `_child` must handle `phaseComplete`, re-declare it in its own `bubbles`, or carry a `"*"` catch-all — otherwise it fails to compile, with the error naming what's missing. Standalone FSMs owe nothing; the contract only exists where FSMs get wired together. The engine never reads `bubbles` — it's purely type-level.

## Deferred input

Call `defer()` inside a handler to queue the current input for replay after the next transition. Pass `{ until: "stateName" }` to replay only on entry to a specific state.

```ts
const fsm = createFsm({
    id: "loader",
    initialState: "loading",
    context: {},
    states: {
        loading: {
            _onEnter() {
                // simulate async load completing
                setTimeout(() => fsm.handle("loaded"), 100);
            },
            // Can't process "save" while still loading — replay it later
            save({ defer }) {
                defer({ until: "ready" });
            },
            loaded: "ready",
        },
        ready: {
            save() {
                // deferred "save" replays here automatically after transitioning in
                console.log("saving");
            },
        },
    },
});

fsm.handle("save"); // deferred — not yet in "ready"
// after "loaded" fires and transitions to "ready", "save" replays automatically
```

## Events

Both `Fsm` and `BehavioralFsm` emit lifecycle events you can subscribe to with `on()`. Use `"*"` to catch everything.

```ts
const sub = light.on("transitioned", ({ fromState, toState }) => {
    console.log(`${fromState} -> ${toState}`);
});

sub.off(); // unsubscribe

// wildcard — receives every event
light.on("*", (eventName, data) => {
    console.log(eventName, data);
});
```

### Built-in events

| Event           | Payload                  | Fired when                              |
| --------------- | ------------------------ | --------------------------------------- |
| `transitioning` | `{ fromState, toState }` | A transition is about to occur          |
| `transitioned`  | `{ fromState, toState }` | A transition completed                  |
| `handling`      | `{ inputName }`          | An input is about to be dispatched      |
| `handled`       | `{ inputName }`          | An input was successfully handled       |
| `nohandler`     | `{ inputName, args }`    | No handler found in current state       |
| `invalidstate`  | `{ stateName }`          | Transition targeted a nonexistent state |
| `deferred`      | `{ inputName }`          | An input was deferred                   |

`BehavioralFsm` events include a `client` field in every payload to identify which client the event pertains to.

## TypeScript

machina is written in TypeScript. State names, input names, and handler signatures are all inferred from the config object — no manual type parameters needed for `createFsm`.

```ts
import type {
    FsmConfig,
    FsmEventMap,
    BehavioralFsmEventMap,
    HandlerArgs,
    HandlerFn,
    StateNamesOf,
    InputNamesOf,
    Subscription,
    DisposeOptions,
} from "machina";

// StateNamesOf and InputNamesOf extract literal unions from your states config:
// type MyStates = StateNamesOf<typeof myConfig.states>; // "green" | "yellow" | "red"
// type MyInputs = InputNamesOf<typeof myConfig.states>; // "tick" | "timeout"
```

Transition targets in string shorthand are validated against actual state keys at compile time. A typo like `timeout: "yellw"` is a type error.

## Disposal

`dispose()` permanently shuts down an FSM. All subsequent method calls are silent no-ops. Child FSMs are disposed by default.

```ts
fsm.dispose(); // also disposes child FSMs

fsm.dispose({ preserveChildren: true }); // leave child FSMs running
```

## Companion tools

- [machina-inspect](https://www.npmjs.com/package/machina-inspect) — static analysis for FSM configs: unreachable states, `_onEnter` loops, missing handlers.
- [machina-test](https://www.npmjs.com/package/machina-test) — Jest/Vitest custom matchers for testing FSM graph topology. Assert reachability and catch dead states from your test suite.
- [eslint-plugin-machina](https://www.npmjs.com/package/eslint-plugin-machina) — ESLint plugin wrapping machina-inspect for inline editor feedback.

## License

MIT + GPL-2.0

---
_Source: https://npm.io/package/machina · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
