createMachine
Creates a state machine definition that can be interpreted into an actor.
Signature
function createMachine< TStateValue extends string, TEvent extends MachineEvent, TContextSchema extends Schema.Schema.Any, R = never, E = never,>(config: MachineConfig): MachineDefinitionType Parameters
| Parameter | Description |
|---|---|
TStateValue | Union of state value strings (e.g., "idle" | "loading" | "done") |
TEvent | Union of event types extending MachineEvent |
TContextSchema | The Effect Schema type (use typeof YourSchema) |
R | Effect requirements (service dependencies) |
E | Effect error type |
Config Properties
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier for the machine |
initial | TStateValue | Yes | Initial state value |
context | Schema.Schema | Yes | Effect Schema for context validation and serialization |
initialContext | TContext | Yes | Initial context value |
states | Record<TStateValue, StateNodeConfig> | Yes | State definitions |
StateNodeConfig
Each state can have the following properties:
| Property | Type | Description |
|---|---|---|
entry | Action[] | Actions to run when entering this state |
exit | Action[] | Actions to run when leaving this state |
on | Record<EventTag, TransitionConfig> | Event handlers |
activities | ActivityConfig[] | Long-running effects active in this state |
invoke | InvokeConfig | One-shot async operation |
after | DelayedTransitionConfig | Delayed/timed transitions |
TransitionConfig
| Property | Type | Description |
|---|---|---|
target | TStateValue | Target state (optional, stay in current if omitted) |
guard | Guard<TContext, TEvent> | Condition for transition |
actions | Action[] | Actions to run during transition |
Returns
Returns a MachineDefinition object with:
| Property | Type | Description |
|---|---|---|
_tag | "MachineDefinition" | Type discriminator |
id | string | Machine identifier |
config | MachineConfig | The original config |
initialSnapshot | MachineSnapshot | Initial state snapshot |
contextSchema | Schema.Schema | The context schema |
Examples
Basic Machine
import { createMachine, assign } from "effstate";import { Data, Schema } from "effect";
// Define eventsclass Increment extends Data.TaggedClass("INCREMENT")<{}> {}class Reset extends Data.TaggedClass("RESET")<{}> {}
type CounterEvent = Increment | Reset;
// Define context schemaconst ContextSchema = Schema.Struct({ count: Schema.Number,});
// Create machine with explicit type parametersconst counterMachine = createMachine< "idle" | "active", CounterEvent, typeof ContextSchema>({ id: "counter", initial: "idle", context: ContextSchema, initialContext: { count: 0 }, states: { idle: { on: { INCREMENT: { target: "active", actions: [assign(({ context }) => ({ count: context.count + 1 }))], }, }, }, active: { on: { INCREMENT: { actions: [assign(({ context }) => ({ count: context.count + 1 }))], }, RESET: { target: "idle", actions: [assign({ count: 0 })], }, }, }, },});With Entry/Exit Actions
const machine = createMachine({ id: "door", initial: "closed", context: ContextSchema, initialContext: { openCount: 0 }, states: { closed: { entry: [effect(() => Effect.log("Door closed"))], on: { OPEN: { target: "open" }, }, }, open: { entry: [ effect(() => Effect.log("Door opened")), assign(({ context }) => ({ openCount: context.openCount + 1 })), ], exit: [effect(() => Effect.log("Leaving open state"))], on: { CLOSE: { target: "closed" }, }, }, },});With Delayed Transitions
Delayed transitions automatically fire after a specified delay. By default, delays are auto-cancelled when exiting the state (preventing the “spam button” bug).
import { Duration, Effect } from "effect";
const machine = createMachine({ id: "timeout", initial: "idle", context: ContextSchema, initialContext: {}, states: { idle: { on: { START: { target: "running" }, }, }, running: { // Simple duration-based delay (auto-cancelled on state exit) after: { delay: Duration.seconds(5), transition: { target: "timedOut" }, }, on: { COMPLETE: { target: "done" }, }, }, timedOut: {}, done: {}, },});Delay formats:
// Numeric shorthand (milliseconds)after: { 5000: { target: "timedOut" }}
// Duration-based (recommended)after: { delay: Duration.seconds(5), transition: { target: "timedOut" }}
// Persistent delay (survives state exits - for global timeouts)after: { delay: Duration.minutes(30), transition: { target: "sessionTimeout" }, persistent: true}
// Effect-based delay (full control with interruption handlers)after: { delay: Effect.sleep(Duration.seconds(5)).pipe( Effect.onInterrupt(() => telemetry.trackCancellation()) ), transition: { target: "timedOut" }}
// Persistent + Effect (both features combined)after: { delay: Effect.gen(function* () { yield* Effect.sleep(Duration.minutes(1)); yield* metrics.recordHeartbeat(); }), transition: { target: "timeout" }, persistent: true}| Option | Description |
|---|---|
delay | Duration.DurationInput or Effect<void, never, R> |
transition | Target state and optional actions |
persistent | If true, delay survives state exits (only cancelled on actor.stop()) |
With Activities
const machine = createMachine({ id: "polling", initial: "idle", context: ContextSchema, initialContext: { data: null }, states: { idle: { on: { START: { target: "polling" } }, }, polling: { activities: [ { id: "pollData", src: ({ send }) => Effect.gen(function* () { while (true) { yield* Effect.sleep("5 seconds"); const data = yield* fetchData(); send(new DataReceived({ data })); } }), }, ], on: { DATA_RECEIVED: { actions: [assign(({ event }) => ({ data: event.data }))], }, STOP: { target: "idle" }, }, }, },});With Invoke
Use the invoke() helper for proper type inference:
import { createMachine, assign, invoke } from "effstate";
const machine = createMachine({ id: "userLoader", initial: "idle", context: ContextSchema, initialContext: { user: null, error: null }, states: { idle: { on: { LOAD: { target: "loading" } }, }, loading: { invoke: invoke({ id: "loadUser", src: ({ context }) => fetchUser(context.userId), onSuccess: { target: "ready", actions: [assign(({ event }) => ({ user: event.output }))], }, onFailure: { target: "error", actions: [assign(({ event }) => ({ error: event.error }))], }, }), }, ready: {}, error: {}, },});