Type-safe state machines for Effect.
Effect Machine gives one actor a schema-first state model, a typed event mailbox, scoped Effect work, typed input and output, supervision, persistence hooks, inspection, and framework-neutral Atom integration.
Use it when a feature has several valid states, invalid transitions, state-owned async work, timeouts, cancellation, actor coordination, or UI views that need precise subscriptions.
bun add effect-machine effecteffect is a required peer dependency.
import { Event, Machine, State } from "effect-machine";
import * as ActorAtom from "effect-machine/atom";
import { EntityMachine, toEntity } from "effect-machine/cluster";Use effect-machine for local machines and actors. Use effect-machine/atom for React, Solid, or another Effect Atom binding. Use effect-machine/cluster for distributed entity machines.
States and events are Effect schemas.
import { Effect, Schema } from "effect";
import { Event, Machine, State } from "effect-machine";
const DownloadState = State({
Idle: {},
Downloading: { url: Schema.String },
Done: { url: Schema.String, bytes: Schema.Finite },
Failed: { url: Schema.String, message: Schema.String },
});
const DownloadEvent = Event({
Start: { url: Schema.String },
Completed: { bytes: Schema.Finite },
Failed: { message: Schema.String },
});
const downloadMachine = Machine.make({
state: DownloadState,
event: DownloadEvent,
initial: DownloadState.Idle,
})
.on(DownloadState.Idle, DownloadEvent.Start, ({ event }) =>
DownloadState.Downloading({ url: event.url }),
)
.on(DownloadState.Downloading, DownloadEvent.Completed, ({ state, event }) =>
DownloadState.Done.with(state, { bytes: event.bytes }),
)
.on(DownloadState.Downloading, DownloadEvent.Failed, ({ state, event }) =>
DownloadState.Failed.with(state, { message: event.message }),
)
.final(DownloadState.Done, ({ state }) => state.bytes)
.final(DownloadState.Failed, () => 0);
const program = Effect.scoped(
Machine.scoped(
Effect.gen(function* () {
const actor = yield* Machine.spawn(downloadMachine);
yield* actor.start;
yield* actor.send(DownloadEvent.Start({ url: "/report.pdf" }));
yield* actor.send(DownloadEvent.Completed({ bytes: 1024 }));
return yield* actor.awaitOutput;
}),
),
);An empty variant is a value such as DownloadState.Idle. A non-empty variant is a constructor such as DownloadState.Downloading({ url }).
State.with(source, fields) copies matching fields into the target variant. It prevents manual context spreading across different states.
Effect Machine does not add an action queue or a second context system.
| Work | API |
|---|---|
| Unconditional state change | .on, .reenter, or .immediate |
| Conditional state change | .when, .reenterWhen, or .immediateWhen |
| Work that produces a completion event | .task |
| State-owned stream or resource | .spawn |
| Actor-owned stream or resource | .background |
| Autonomous machine sequence | Machine.run with Effect.flatMap |
| Interactive multi-phase flow | Parent machine with child actors |
Effect requirements remain in R. A machine cannot start until the application provides every required service. Effectful transition handlers must have never in their error channel. Convert expected failures to states or events.
Machine-lifetime backgrounds can read self.state and self.latestTransition. These are the
actor-owned subscription refs. They stay stable across supervision generations. Treat them as
read-only and use SubscriptionRef.get or SubscriptionRef.changes to observe them.
Read the Effect model and async work ownership.
Register ordered candidates for one state and event. The first passing guard wins. An unguarded candidate is the fallback.
machine
.when(
State.Checking,
Event.Continue,
function hasStock({ state }) {
return state.stock > 0;
},
() => State.Accepted,
)
.on(State.Checking, Event.Continue, () => State.Rejected)
.immediate(State.Accepted, ({ state }) => State.Ready.with(state));The predicate can return a Boolean or Effect<boolean, never, R>. Its requirements flow into the machine type. actor.can(event) evaluates the same predicate with the actor's captured context. ActorAtom.can(actor, event) exposes the result to React and Solid. The inspector uses the predicate function name.
Immediate transitions run until the state is stable. Subscribers see only the stable state. The runtime stops an accidental eventless loop after 100 edges.
class Api extends Context.Service<
Api,
{ readonly load: (id: string) => Effect.Effect<Data, ApiError> }
>()("app/Api") {}
machine.task(State.Loading, ({ state }) => Effect.flatMap(Api, (api) => api.load(state.id)), {
name: "load-data",
onSuccess: (data) => Event.Loaded({ data }),
onFailure: (error) => Event.LoadFailed({ message: String(error) }),
});
const actor = yield * Machine.spawn(machine).pipe(Effect.provide(ApiLive));
yield * actor.start;The actor captures the Effect context during allocation. It keeps those services when it starts later.
onFailure receives the typed Effect error. A defect does not enter onFailure. It stops the actor or starts supervision.
const checkoutMachine = Machine.make({
state: CheckoutState,
event: CheckoutEvent,
initial: (input: CheckoutInput) => CheckoutState.Reviewing(input),
}).final(CheckoutState.Done, ({ state }) => ({ receiptId: state.receiptId }));
const program = Machine.run(cartMachine).pipe(
Effect.flatMap((cart) => Machine.run(checkoutMachine, { input: cart })),
);Machine.run starts one actor, waits for output, and always stops it. Interruption releases actor resources. The final actor state remains available when you use Machine.spawn and retain the actor reference.
Use a parent machine when a UI must route between phases, keep shared values, or support back navigation. Read Actors and systems.
| Member | Use |
|---|---|
start |
Start a direct actor |
send(event) |
Queue an event |
call(event) |
Process an event and return transition information |
ask(event) |
Return a typed reply from an Event.reply event |
waitFor(state) |
Wait for a state constructor or predicate |
sendAndWait(event, state) |
Send and wait for a state |
snapshot |
Read the current state as an Effect |
awaitFinal |
Wait for the final state |
awaitOutput |
Wait for typed output |
awaitExit |
Wait for Final, Stopped, or Defect |
drain |
Process queued events and stop |
subscribe |
Observe state with a host callback |
client |
Use the actor outside Effect |
system |
Access named actors |
children |
Read direct child actors |
Machine.spawn returns an unstarted actor. system.spawn starts the actor.
Use actor.client in a JavaScript callback or application that does not run inside Effect. client.can(event) returns a Promise and supports Effect predicates. client.canSync(event) supports Boolean predicates only. React and Solid should use Actor Atoms.
import * as ActorAtom from "effect-machine/atom";
const stateAtom = ActorAtom.make(actor);
const countAtom = ActorAtom.select(stateAtom, (state) => state.count);The selected Atom stays writable. Writes send machine events. A selector publishes only when its selected value changes.
The React example uses useAtomSuspense and Motion. The Solid example uses useAtomResource, Suspense, and solid-transition-group. Both include performance tests. Both retain exit-animation data in the terminal machine state.
Read Atom and UI integration and browse all examples.
- Recovery resolves state during actor startup.
- Durability saves committed transitions.
- Supervision restarts defects within an Effect
Schedulebudget. - Inspection reports events, named transition operations, transitions, named guards, tasks, Effects, errors, stops, and actor generations.
- Typed
inspectspawn options observe one actor. actor.system.inspectlets a late tool observe all actors in one system.
Read Persistence and supervision and Inspection.
Use simulate or createTestHarness for transition paths. Spawn a real actor for tasks, services, resources, persistence, supervision, inspection, and actor topology.
const result = yield * simulate(machine, events, { input });
yield * assertPath(machine, events, ["Idle", "Loading", "Done"]);
yield * assertNeverReaches(machine, events, "Failed");Read Testing.
The migration guide covers context, assign, actions, invoked promise and callback actors, root routers, actor registries, selectors, inspection, persistence, and exit animation values. Its patterns come from a large XState kiosk application.
Use effect-machine/cluster to expose a machine through Effect Cluster. It supports typed send, ask, state reads, state watches, input adapters, snapshot persistence, and journal persistence.
Read Cluster entities.
The examples directory is a Bun workspace.
bun run examples:gate
bun run example:react
bun run example:solidThe example matrix links every pattern to executable code.
- Effect model
- Async work
- Actors and systems
- Atom and UI integration
- Persistence and supervision
- Inspection
- Testing
- Migration from XState
- Cluster entities
- AI agent reference
MIT