Quick Start

Install

npm install @1state/core
npm install --save-dev @1state/validate @1state/mermaid

Node 20+. ESM only — there is no CommonJS build.

A flow is an object

One state per question. An event records the answer as a self-transition, and a go guard leaves once the answer is on the context.

import { createMachine, assign } from '@1state/core'

const signup = {
  context: { email: null, password: null },
  initial: 'email',
  states: {
    email: {
      on: {
        answer: { target: 'email', actions: assign({ email: (_ctx, e) => e.value }) },
      },
      go: [{ target: 'password', cond: (ctx) => ctx.email !== null }],
    },
    password: {
      on: {
        answer: { target: 'password', actions: assign({ password: (_ctx, e) => e.value }) },
      },
      go: [{ target: 'done', cond: (ctx) => ctx.password !== null }],
    },
    done: { type: 'final' },
  },
}

answer targets the state it fires in. The state exits and re-enters, so its go guards run again against the answer that was just recorded.

Run it

const machine = createMachine(signup)

await machine.start()
// { value: "email", context: { email: null, password: null }, done: false }

await machine.send('answer', { value: 'a@example.org' })
// { value: "password", ... }

await machine.path()
// ["email", "password"]

await machine.send('answer', { value: 'hunter2' })
// { value: "done", done: true }

start() enters email, retries its go transitions, finds the guard closed, and stops. That is where the user is.

Resume it

Load the stored answers into context and start. Nothing else.

const resumed = createMachine(signup, { context: { email: 'a@example.org' } })
await resumed.start() // value: "password"

There was no cursor to load. start() entered email, found its guard open, moved to password, found that guard closed, and stopped.

Write through named scripts

Inline functions are fine in a test. In an application, name the actions in the definition and point them at your database through options.scripts, so the definition does not change between the two.

const machine = createMachine(signup, {
  scripts: {
    saveEmail: (ctx, e) => db.account.update(ctx.id, { email: e.value }),
  },
  context: await load(accountId),
})

A script is a function (context, event) — sync or async — or the name of one in options.scripts.

Check it

npx 1state-validate signup.js
npx 1state-mermaid --title 'Signup' signup.js > SIGNUP.md

1state-validate runs a JSON Schema pass, a reachability pass and a compile through the engine, and reports everything at once. 1state-mermaid --check fails when a committed diagram no longer matches its machine, which is what makes it a CI job.

Next

Read Derived position. It is one page, it is the whole model, and the rule at the bottom of it is the one mistake worth avoiding.