effect-state-machine
Tutorials

Build your first machine

Create and run a typed counter workflow from an empty TypeScript project.

In this tutorial, we will build a counter workflow that starts in Ready, accepts increments in Counting, and completes in Done. Running it will print three observable snapshots:

initial: { _tag: 'Ready', count: 1 }
counting: { _tag: 'Counting', count: 4 }
completed: { _tag: 'Done', count: 4 }

By the end, you will have defined a machine's data and behavior, started a scoped instance, sent events to it, and read its completion value.

Prerequisites

Start in an empty directory with Node.js 26 and pnpm 11 available. This tutorial uses effect-state-machine, effect, TypeScript, and tsx.

Create the project

Initialize the project and install the pinned dependencies:

pnpm init
pnpm add effect-state-machine effect
pnpm add --save-dev typescript tsx

Create index.ts with the following program:

packages/core/examples/FirstMachine.ts
/** Complete program used by the first-machine documentation tutorial. */
import * as Console from "effect/Console"
import * as Effect from "effect/Effect"
import * as Schema from "effect/Schema"
import * as Machine from "effect-state-machine/Machine"
import * as MachineEngine from "effect-state-machine/MachineEngine"

const Input = Schema.Struct({ initialCount: Schema.Number })

const State = Machine.taggedUnion({
  Ready: {
    fields: { count: Schema.Number },
    description: "Wait for counting to start.",
  },
  Counting: {
    fields: { count: Schema.Number },
    description: "Accept counter updates.",
  },
  Done: {
    fields: { count: Schema.Number },
    description: "Finish with the final count.",
  },
})

const Event = Machine.taggedUnion({
  Start: { fields: {} },
  Increment: { fields: { amount: Schema.Number } },
  Finish: { fields: {} },
})

const counter = Machine.builder({ input: Input, state: State, event: Event })

const definition = counter.define(
  {
    id: "counter",
    idempotencyKey: ({ initialCount }) => String(initialCount),
    initial: ({ initialCount }) => ({ _tag: "Ready", count: initialCount }),
  },
  {
    Ready: counter.state({
      Start: {
        target: "Counting",
        reduce: ({ state }) => ({ count: state.count }),
      },
    }),
    Counting: counter.state({
      Increment: {
        stay: ({ state, event }) => ({ count: state.count + event.amount }),
      },
      Finish: {
        target: "Done",
        reduce: ({ state }) => ({ count: state.count }),
      },
    }),
    Done: counter.final(),
  },
)

const program = Effect.scoped(
  Effect.gen(function* () {
    const handle = yield* definition.run({ initialCount: 1 })

    const initial = yield* handle.snapshot
    yield* Console.log("initial:", initial)

    yield* handle.send({ _tag: "Start" })
    yield* handle.send({ _tag: "Increment", amount: 3 })

    const counting = yield* handle.snapshot
    yield* Console.log("counting:", counting)

    yield* handle.send({ _tag: "Finish" })
    const completed = yield* handle.completion
    yield* Console.log("completed:", completed)
  }),
).pipe(Effect.provide(MachineEngine.layerMemory()))

await Effect.runPromise(program)

Notice that the definition record has exactly one key for every state Schema tag. Each key narrows state and event inside its handlers. stay updates Counting without exiting and re-entering it. idempotencyKey derives one logical machine identity from the input. The explicit memory layer is the persistence choice for this tutorial; one layer value shares its volatile database across every run beneath it.

Run the machine

Execute the program:

pnpm exec tsx index.ts

The output should look like this:

initial: { _tag: 'Ready', count: 1 }
counting: { _tag: 'Counting', count: 4 }
completed: { _tag: 'Done', count: 4 }

The first snapshot comes from initial. Each call to send waits until its event has been processed. Entering the Done final state resolves completion with that final state value.

Explore the same machine in Studio

Select Start counting, Increment by 3, and Finish in order. Notice that the active node, state value, and History panel follow the three snapshots printed by the program.

Starting the counter machine…

You can now define and run a machine with ordinary transitions. Continue with Run an Effect when a state becomes active when a state must own asynchronous work, or consult the generated Machine API for the complete builder surface.

On this page