Code-first · Effect-native

Write the machine. Watch it run.

State machines for Effect. Model checkouts, syncs, and retries as explicit states and typed events — impossible states can't happen, every transition has a name, and the definition you author — or your agent writes — is the one Studio shows you running.

checkout.ts — current API
executable + inspectable
const definition = checkout.define(
  {
    id: "checkout",
    idempotencyKey: () => "checkout",
    initial: () => ({ _tag: "Browsing", items: 0 }),
  },
  {
    Browsing: checkout.state({
      AddItem: { target: "Browsing", reduce: … },
      RemoveItem: { target: "Browsing", reduce: … },
      BeginCheckout: { branches: [{ when: cartHasItems, target: "Checkout" }] },
    }),
    Checkout: checkout.state({ SubmitOrder: …, BackToShop: … }),
    PlacingOrder: checkout.invoke({
      name: "Orders.place",
      success: Schema.String, error: PaymentDeclined,
      // an Effect that needs a service — its Layer still comes later
      effect: (state) => Effect.flatMap(Orders, ({ place }) => place(state.items)),
      onSuccess: { target: "Ordered", reduce: ({ value }) => ({ orderId: value }) },
      onFailure: { target: "PaymentFailed" },  // typed PaymentDeclined
    }),
    PaymentFailed: checkout.state({ RetryPayment: …, BackToShop: … }),
    Ordered: checkout.final(),
  },
)

// a scoped Effect — Orders is inferred, provided by a Layer
const handle = yield* definition.run({}).pipe(
  Effect.provide(OrdersLive),
  Effect.provide(MachineEngine.layerMemory()),
)

Every click below is an event.

The Bug Emporium is an ordinary Effect app: one definition.run, one explicit MachineEngine Layer, and an embedded Studio bound to the live handle. Buy some bugs — get declined, retry — and the behavior map, snapshot, and semantic history follow. Move the history cursor to inspect an older step; the running shop never notices.

<Studio machine={{ definition, handle }} /> — inspecting the live handle
bug-emporium.example/shop
The Bug Emporiumjar: 0 bugs · $0
artisanal, free-range bugs · sourced from production
shy
Heisenbug · $404wild-caught · disappears when observed
× 0
2 for 1
Race Condition · $21free-range · first come, first served. twice.
× 0
−1bestseller
Off-by-One · $99farm-raised · you get n−1, pay for n
× 0
Starting the checkout machine…

Built for Effect, not adapted to it.

Dependencies stay services provided by Layers. Expected failures stay typed. Cancellation uses Scope and fibers, retry uses native Schedule. The handle is Effect-native: no Promise methods, no global runtime. Every definition runs through an explicit MachineEngine backed by the MachineStore you choose.

$pnpm add effect-state-machine effect

What the definition actually is

One immutable value describes input, states, events, transitions, timers, regions, invoked work, and children. Tooling reads that value — it never executes Effects to discover the graph. Agents make code cheap to write; the graph keeps it cheap to review.

valueMachine definition

Immutable authored value. Inspectable without running Effects.

effectdefinition.run

Scoped Effect. The engine and invoked-work requirements remain explicit Layers.

apiHandle

snapshot, changes, send, can, completion, inspection.

devtoolStudio

Embed it in React or connect browser and Node machines to the standalone app. Remote attachment is scoped, lazy, and inert when Studio is closed.

graphRead-only graph

The definition projected for humans reviewing code — yours or your agent's. Never a second editable source of truth.

runtimeRestart-capable engine

Store-backed checkpoints retain state, mailbox order, absolute timer deadlines, dispatch keys, and encoded work outcomes across process loss.

Keep the behavior you can still explain.

effect-state-machineno bugs were harmed in this demoDocs · GitHub · MIT