NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #4208 most downloaded on npm
Finite State Machines and Statecharts for the Modern Web.
Last release 2 days ago
03 Oct 2026
Ships on a steady schedule
a new release about every 9 days
Nearly every release is documented
notes for 60 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
9 years old
340 releases · first in 2017
26378e5 : Add experimental pure actor-system transitions with immutable system snapshots, external effects as data, and chronological virtual time acr
26378e5: Add experimental pure actor-system transitions with immutable system snapshots,
external effects as data, and chronological virtual time across actors.
const systemLogic = { root: machine };
const [snapshot] = initialSystemTransition(systemLogic, { input });
const [nextSnapshot, effects] = systemTransition(systemLogic, snapshot, snapshot.root, event);
const [laterSnapshot] = advanceSystemTime(systemLogic, nextSnapshot, { time: 4000 });SimulatedClock now runs callbacks at each timer's deadline before reaching the
requested time, including intermediate timers created during those callbacks.
Large timer batches advance efficiently while preserving deadline and insertion
order. Subscription completion/error mappings arrive before native child
notifications, matching live actors.
295a705: Durable executions preserve timer deadlines in checkpoints persisted after executeEffects() succeeds. Adapters can provide an absolute now() clock shared across restores and replay. Root error snapshots no longer also trigger an unhandled global throw; explicit hosts handle the snapshot, while run() rejects with its error.
createMachineFromConfig() retains named actor sources so .provide({ actors }) can replace them, including when restoring children.
Reserved xstate.* transition descriptors are accepted without losing exact declared event payload types. Machines created from never configs no longer trigger excessive type instantiation in generic consumers.
await execution.executeEffects(effects);
const checkpoint = machine.getPersistedSnapshot(snapshot);8e509ae: Add setup(...).createInvoke(...) for typed inline invocations. The helper infers actor input, completion output, errors and snapshots from its src logic. Async functions can be authored directly in src, with optional schemas.input, schemas.output and schemas.error. Without an output schema, completion output is inferred from the async return value. Inline calls also infer the enclosing state's narrowed context, state input and transition targets, including alongside registered actor sources.
Actors whose input excludes undefined require an input value or mapper. Invokes check their child IDs and source compatibility against schemas.children declared in either the setup or machine.
invoke: s.createInvoke({
schemas: { input: types<{ userId: string }>() },
input: ({ context }) => ({ userId: context.userId }),
src: async ({ input }) => ({ name: input.userId }),
onDone: ({ event }) => ({
context: { name: event.output.name }
})
})One column per quarter.
d6dbf41 : Add experimental execution.restore(persistedSnapshot) to xstate/durable for resuming checkpoints without sending a synthetic event or replay
d6dbf41: Add experimental execution.restore(persistedSnapshot) to xstate/durable for
resuming checkpoints without sending a synthetic event or replaying entry
actions. Execute the returned effects to resume active embedded children and
pending timers. Restored child startup uses the host adapter, including nested
children. Timers with a persisted wall-clock start keep their original deadline,
including time spent waiting to execute effects or starting children.
const execution = createDurable(machine, {
...adapter,
transitionIndex: checkpoint.nextTransitionIndex
});
const [snapshot, effects] = execution.restore(checkpoint.snapshot);
await execution.executeEffects(effects);The deprecated top-level internalEvents machine config key and the deprecated state actor option are removed.
9003cf1: ### Removed
The deprecated top-level internalEvents machine config key and the deprecated state actor option are removed.
Declare private events in schemas.internalEvents:
// Before
createMachine({
schemas: { events: { start: z.object({}), tick: z.object({}) } },
internalEvents: ['tick'] as const
// ...
});
// After
createMachine({
schemas: {
events: { start: z.object({}) },
internalEvents: { tick: z.object({}) }
}
// ...
});Restore a persisted snapshot with the snapshot option:
// Before
createActor(machine, { state: persistedSnapshot });
// After
createActor(machine, { snapshot: persistedSnapshot });In development builds, a config with a top-level internalEvents key throws an error naming schemas.internalEvents, and passing state to createActor(...) throws an error naming snapshot.
5228c00: Infer the current service requirements of actions and actors replaced with machine.provide. Require declared actor input in createActorAtoms, consistently with createEffectActor.
Effect tasks and streams now release their resources when they complete, fail or are cancelled. Actor shutdown waits for task cleanup before releasing resources shared for the actor's lifetime. Use withActorScope around an acquisition to keep its resource until the owning Effect actor stops:
import { Effect } from 'effect';
import { withActorScope } from '@xstate/effect';
const session = Effect.acquireRelease(
Effect.succeed({ id: 'session' }),
() => Effect.log('Session closed')
).pipe(withActorScope);Improve XState Effect guides with complete, tested workflow, stream, inspection and React examples.
77cad04: Resolve after delays and state timeout functions with context updated by the same state's entry function.
State timers are scheduled after the entry function's queued actions. Entry cancellation runs before scheduling; cancellation from a later event handler still cancels an active timer.
waiting: {
entry: () => ({ context: { ms: 300 } }),
after: { d: { target: 'done' } }
}
// With delays: { d: ({ context }) => context.ms }, waits 300ms.The deprecated type aliases NoInfer (use the built-in NoInfer ), AnyInterpreter (use AnyActor ), and ResolvedStateMachineTypes .
3e024f9: createAsyncLogic accepts schemas.error. It types the actor's error snapshot field and event.error in the invoking machine's onError. Without it, the error stays unknown, so reading properties from it is a type error.
const fetchUser = createAsyncLogic({
schemas: {
output: z.object({ name: z.string() }),
error: z.object({ code: z.string() }),
},
run: async () => ({ name: "David" }),
});
setup({ actors: { fetchUser } }).createMachine({
invoke: {
src: "fetchUser",
onError: ({ event }) => {
event.error.code; // string
},
},
});Without schemas.error, narrow event.error before reading from it.
With a timeout, the error type also includes TimeoutError, so narrow before reading schema fields:
onError: ({ event }) => {
if (event.error instanceof TimeoutError) return;
event.error.code; // string
};3e024f9: Children declared in schemas.children now contribute their completion events to the event union seen by entry, exit, guards and transition functions. assertEvent(event, 'xstate.done.actor') narrows event.output to the child's output type, and event.actorId to the declared ids.
setup({
actors: { fetchUser },
schemas: {
children: { fetch: z.custom<ActorRefFromLogic<typeof fetchUser>>() }
}
}).createMachine({
invoke: { id: 'fetch', src: 'fetchUser' },
entry: ({ event }) => {
assertEvent(event, 'xstate.done.actor');
event.output.name; // string
}
});Code that assumed every event in these positions is a declared public event may need a narrowing check first.
73fa80b: Entry and exit functions now receive stateNode, the state node being entered or exited.
createMachine({
initial: 'a',
states: {
a: {
entry: ({ stateNode }, enq) => {
enq(() => console.log('Entered', stateNode.id));
}
}
}
});11c6f52: createFSM from xstate/fsm now follows the (snapshot, event) => [snapshot, effects] protocol used by all actor logic. fsm.transition(...) returns a [nextSnapshot, effects] tuple, where effects is always empty, and snapshots include status: 'active'. An FSM can now run in createActor and be passed to transition() and initialTransition().
Before:
let state = fsm.initialState;
state = fsm.transition(state, { type: 'toggle' });After:
let state = fsm.initialState;
[state] = fsm.transition(state, { type: 'toggle' });
// Run it as an actor
import { createActor } from 'xstate';
const actor = createActor(fsm).start();
actor.send({ type: 'toggle' });
actor.getSnapshot().value; // 'active'97e9166: getMicrosteps() and getInitialMicrosteps() now return the transitions taken in each microstep as a third tuple element, including eventless transitions and transitions for raised events.
import { getMicrosteps } from 'xstate';
for (const [snapshot, actions, transitions] of getMicrosteps(
machine,
snapshot,
event
)) {
console.log(transitions.map((t) => `${t.source.id} -> ${t.eventType}`));
}d62cdc7: One event now has a single microstep bound: options.maxIterations, which defaults to 1000. Exceeding it throws the new exported InfiniteTransitionError, whose message names the actor id, the event and the last five states visited. Previously a hard-coded limit of 1000 applied regardless of maxIterations, so raising the limit had no effect.
import { createMachine, InfiniteTransitionError } from 'xstate';
const machine = createMachine({
options: { maxIterations: 5000 },
// ...
});aa49aee: ### Removed
createFSM and the FSM* types are no longer exported from the root xstate entry. Import them from xstate/fsm.xstate/actions, xstate/guards, xstate/invoke, and xstate/dev folders are no longer published.xstate/graph: getStateNodes(stateNode) is renamed to getDescendantStateNodes(stateNode) so it no longer shares a name with the root getStateNodes(stateNode, stateValue).import { createFSM } from 'xstate/fsm';
import { getDescendantStateNodes } from 'xstate/graph';8576291: Remove the @xstate.deadletter inspection event; observe undelivered events with the onRejectedEvent option. The inspection protocol is now exactly @xstate.actor and @xstate.transition. In @xstate/effect, deadLetters(actor) now streams EventRejection objects.
createActor(machine, {
onRejectedEvent: (rejection) => {
console.log(rejection.event.type, rejection.reason, rejection.issues);
}
});Undelivered events are also available through system.onRejectedEvent(listener), which accepts any number of listeners added at any time and returns a subscription. The onRejectedEvent option registers a listener the same way.
const subscription = actor.system.onRejectedEvent((rejection) => {
console.log(rejection.event.type, rejection.reason);
});
subscription.unsubscribe();aa49aee: ### Removed
getInitialSnapshot(logic, input?) and getNextSnapshot(logic, snapshot, event). Use initialTransition(...) and transition(...), which return [snapshot, effects].NoInfer (use the built-in NoInfer), AnyInterpreter (use AnyActor), and ResolvedStateMachineTypes.import { initialTransition, transition } from 'xstate';
const [initial] = initialTransition(machine, input);
const [next] = transition(machine, initial, { type: 'NEXT' });97e9166: Removed createTestModel and TestModel from xstate/graph; use @xstate/test. The types used only by them (TestModelOptions, TestParam, TestPath, TestPathResult, TestStepResult, TestMeta, EventExecutor) and createShortestPathsGen/createSimplePathsGen are removed too. xstate/graph keeps its path traversal functions.
// Before
const model = createTestModel(machine, {
events: [
{ type: 'SUBMIT', zip: '12345' },
{ type: 'SUBMIT', zip: 'abc' },
{ type: 'CANCEL' }
]
});
for (const path of model.getShortestPaths()) await path.test(params);
// After: `events` is keyed by event type; each payload becomes a named case
import * as fc from 'fast-check';
import { testPaths } from '@xstate/test';
await testPaths(machine, {
events: {
SUBMIT: [
{ case: 'valid', generate: fc.constant({ zip: '12345' }) },
{ case: 'invalid', generate: fc.constant({ zip: 'abc' }) }
]
},
samples: 1,
sut
});Event types without a payload, such as CANCEL, need no entry.
73fa80b: createActor(machine) now requires input when the machine declares an input schema whose type does not accept undefined. Restoring from a persisted snapshot does not require input.
const machine = setup({
schemas: { input: z.object({ id: z.string() }) }
}).createMachine({});
createActor(machine); // type error
createActor(machine, { input: { id: 'a' } }); // ok
createActor(machine, { snapshot: persisted }); // okaa49aee: ### Removed
xstate/scxml entry point moved to the new @xstate/scxml package. xstate no longer depends on saxes.// Before
import { createMachineFromSCXML } from 'xstate/scxml';
// After (npm i @xstate/scxml)
import { createMachineFromSCXML } from '@xstate/scxml';d62cdc7: enq.sendTo(...) to a missing target no longer errors the sending actor. Sending to an undefined ref, to a child id with no running child, or to parent from a root actor now produces a dead letter with reason 'missingTarget': the actor stays active, onRejectedEvent receives the event (with targetId and sourceRef), and development builds log a warning naming the sender and the target. State onError handlers no longer receive xstate.error.communication for these sends.
const actor = createActor(machine, {
onRejectedEvent: (rejection) => {
if (rejection.reason === 'missingTarget') {
console.log(rejection.event, rejection.targetId);
}
}
});d62cdc7: Actors are single-use. Calling start() on an actor after stop() now throws Actor <id> was stopped and cannot be restarted. Create a new actor with createActor(). in all builds, instead of silently doing nothing. Calling start() on a running actor, or on an actor that already completed or errored, is still a no-op.
actor.stop();
actor.start(); // throws
const next = createActor(machine).start();3e024f9: When delays are declared (setup({ delays }) or createMachine({ delays })), each after key must be a declared delay name, a number of milliseconds or a duration string such as '5s'. The error now names the offending key. Duration strings are no longer rejected when named delays are declared. Duration keys are checked against the forms the runtime parses: integer milliseconds ('250ms'), decimal seconds ('1.5s') and ISO 8601 durations ('PT1M30S'). Malformed keys such as 'Pfoo' or '1.5ms' are type errors.
setup({ delays: { retryDelay: 1_000 } }).createMachine({
initial: 'waiting',
states: {
waiting: {
after: {
// Type error: Delay 'retryDelya' is not declared in delays.
retryDelya: { target: 'retrying' }
}
},
retrying: {}
}
});Fix the name, or declare the delay in delays.
At runtime, a delay that is neither a configured delay name nor a valid duration string now errors the actor with Invalid delay "…" instead of firing immediately.
3e024f9: When schemas.events is declared, every key in a state's on map must match a declared event type. Wildcards ('*', 'user.*') and reserved xstate.* event types remain allowed. Machines without schemas.events are unchanged.
setup({
schemas: { events: { toggle: z.object({}) } }
}).createMachine({
on: {
// Type error: Event type 'toggel' is not declared in schemas.events.
toggel: { target: '.active' }
}
});Fix the typo, or declare the event in schemas.events.
d62cdc7: Unhandled events are now observable.
transition(logic, snapshot, event) returns the same snapshot object and no effects when no transition handles the event. A handled event always returns a new snapshot object, including a transition function that returns {}.isUnhandled(previousSnapshot, result) helper.onUnhandledEvent(event, snapshot) option for createActor(...).xstate.* events are not reported.import { createActor, isUnhandled, transition } from 'xstate';
const result = transition(machine, snapshot, { type: 'unknown' });
isUnhandled(snapshot, result); // true
createActor(machine, {
onUnhandledEvent: (event, snapshot) => {
console.log(`${event.type} not handled in`, snapshot.value);
}
});ef251fa: Development builds now report leftover v5 configuration (cond, types, string actions, …) with the v6 replacement instead of ignoring it.
// Before: `cond` was silently ignored, so the transition was always taken
createMachine({
initial: 'idle',
states: {
idle: {
on: { submit: { target: 'sending', cond: ({ context }) => context.valid } }
},
sending: {}
}
});
// Now throws: Transition "submit" in state "(machine).idle" uses "cond",
// which was removed. Use an inline transition function instead: ...
// After
createMachine({
initial: 'idle',
states: {
idle: {
on: {
submit: ({ context }) => {
if (!context.valid) return;
return { target: 'sending' };
}
}
},
sending: {}
}
});cond, object-form guard, transition actions, non-function entry/exit, types, tsTypes and schema throw. services, activities, predictableActionArguments, preserveActionOrder, strict and devTools log a warning. Machines built with createMachineFromConfig or createMachineFromSCXML are not checked.
0fe9afe: React Fast Refresh keeps the running actor and its state when you edit a machine. In development builds, useActorRef(), useActor() and useMachine() switch the running actor to the edited machine, so the current state and context are kept, including context that holds DOM elements or cyclic objects. If the edited machine cannot represent the current state, the actor restarts from the edited machine. Production builds are unaffected.
73fa80b: Final states are now inert everywhere, including final regions of a parallel state, matching SCXML: they take no transitions and their invoked actors are not created or started. In development, createMachine warns when any final state declares invoke, on or after.
Move transitions off a final region onto a non-final state:
region: {
initial: 'active',
states: {
active: { on: { NEXT: { target: 'done' } } },
done: { type: 'final' }
}
}8576291: Persisting a snapshot whose context contains a circular reference now throws a descriptive error instead of a RangeError (maximum call stack size exceeded). Shared references that are not circular still persist.
const node: Record<string, unknown> = {};
node.self = node;
const machine = createMachine({ id: 'tree', context: { node } });
createActor(machine).getPersistedSnapshot();
// Error: Cannot persist actor "tree": circular reference at context.node.self8576291: In development builds, getPersistedSnapshot() warns when context, output, error or state inputs contain a value that does not survive a JSON round-trip: a function, symbol, BigInt, NaN or Infinity, Map, Set or circular reference. The warning names the path of the first such value.
d62cdc7: Restoring a snapshot whose state has always transitions or is a choice state now logs a development warning: restored snapshots are not re-evaluated, so those eventless transitions do not run until the next event.
d62cdc7: A persisted snapshot that fails to restore (for example, an unknown state in value or a machine id mismatch) now produces a full machine snapshot with status: 'error' and the failure as error, instead of a bare { status, output, error } object. snapshot.matches(...), snapshot.can(...) and the other snapshot methods keep working.
d62cdc7: Restoring a persisted snapshot with status: 'stopped' now yields a stopped actor. Previously the restored actor kept processing events, running transitions and actions while reporting status: 'stopped'.
d62cdc7: A transition function that returns a promise now throws a descriptive execution error (recoverable with a state onError) instead of leaving the actor unchanged with an internal error. The returned promise's rejection is observed, so no unhandled rejection is reported. Calling enq.* after the transition function returned now throws in development and does nothing in production.
6b1a631 : Machine snapshots keep their machine-specific methods, such as snapshot.matches(...) , when initialization fails (for example, when the cont
69b6663 : Stop child actors and their timers and subscriptions when an unhandled parent error occurs, including when a stop action has not executed ye
69b6663: Stop child actors and their timers and subscriptions when an unhandled parent error occurs, including when a stop action has not executed yet.
Keep useActorRef observers subscribed when the actor is replaced, and subscribe before the replacement starts.
69b6663: Support state names and effect keys such as __proto__, constructor, and toString, including persisted effect restoration. Run every attachment cleanup when an earlier cleanup throws, while preserving the original error.
69b6663: Fix published TypeScript declarations so applications can check XState with skipLibCheck: false.
69b6663: Restore callback subscriptions and active keyed effects. Retry interrupted local async steps while reusing completed outcomes and sharing concurrent same-key work. Pending step callers reject when their actor terminates. Interrupted external side effects require idempotency keys.
Keep SCXML condition errors and transition evaluation isolated between actors, and process condition errors without waiting for state entry.
Replay finite graph event sequences without exploring every reachable state, initialize graph traversal once, and support arbitrary serialized state and event keys. Improve adjacency traversal for large graphs. Keep simulated clocks usable after a timer callback throws.
69b6663: Events declared in schemas.internalEvents are now excluded from actor.send and actor.trigger in the published type declarations, matching the behavior already available when building against source. Both exact keys and wildcard keys are excluded.
const uploadMachine = setup({
schemas: {
events: { start: types<{}>() },
internalEvents: {
tick: types<{}>(),
'progress.*': types<{ bytes: number }>()
}
}
}).createMachine({
/* ... */
});
const actor = createActor(uploadMachine);
actor.send({ type: 'start' }); // ok
actor.send({ type: 'tick' }); // type error
actor.send({ type: 'progress.chunk', bytes: 256 }); // type error
actor.trigger.tick(); // type error: `tick` is not on `trigger`69b6663: A persisted snapshot's children is now typed, so reading a persisted child no longer needs a cast. The new PersistedActorRef type describes both forms: an embedded child carries its own snapshot, while a child persisted by address carries remote: true and leaves its state with the runtime that owns it.
const persisted = actor.getPersistedSnapshot({ embedChildren: false });
persisted.children.auditor.address; // string | undefined
persisted.children.auditor.src; // string69b6663: A leftover v5 types key in a machine config is now a compile error instead of being accepted and silently ignored. The error names the replacement:
createMachine({
// Error: `types` was replaced by `schemas` in v6. Declare `context`,
// `events` and the other contracts under `schemas`, or run
// `xstate-codemod migrate --transform types-to-schemas`.
types: {} as { context: { count: number } },
context: { count: 0 }
});Declare the contracts under schemas instead:
createMachine({
schemas: { context: types<{ count: number }>() },
context: { count: 0 }
});a50ea84 : Machine output is now inferred as the union of the top-level final states' output types when no schemas.output or root output is declared. P
a50ea84: Machine output is now inferred as the union of the top-level final states' output types when no schemas.output or root output is declared. Previously, declaring schemas.output was required to get a typed result from toPromise(actor) or snapshot.output.
const machine = setup({}).createMachine({
initial: 'working',
states: {
working: {
on: {
resolve: { target: 'succeeded' },
reject: { target: 'failed' }
}
},
succeeded: {
type: 'final',
output: () => ({ status: 'ok' as const })
},
failed: {
type: 'final',
output: { status: 'error' as const }
}
}
});
// OutputFrom<typeof machine> is
// { status: 'ok' } | { status: 'error' }98160ed: Importing only xstate/fsm now typechecks on its own. Previously, an fsm-only program failed with Property 'observable' does not exist on type 'SymbolConstructor' errors because the Symbol.observable type augmentation lived in the main entry. The xstate/fsm entry also no longer pulls the main entry's full type surface into the program, so editors and tsc check far less code for fsm-only consumers.
50184a8 : Fixed declaration emit for machines and setups created with setup({ states }) . A package that exported one could not be built with declarat
50184a8: Fixed declaration emit for machines and setups created with setup({ states }).
A package that exported one could not be built with declaration: true: the
emitted types reached for ActiveStateContext and a handful of private marker
types that were never exported from the package entry point, so consumers saw
TS2742 ("cannot be named without a reference to xstate/dist/...") or TS4023 on
xstate's internal unique symbols.
The types declaration emit needs are now public — ActiveStateContext,
RootContextMarker, ChoiceStateNodeConfig, RegularStateNodeConfig, and the
strict-target markers — and the private state-schema symbols live behind named
marker types instead of inline computed keys, so emit references a name rather
than expanding a symbol it cannot write down.
f7642bf : Fixed generic type helpers that accidentally restricted invocation transition metadata, state input, and transition children. AnyInvokeDefin
AnyInvokeDefinition, AnyStateNodeConfig, and AnyTransitionConfigFunction now preserve arbitrary types in these positions when inspecting or accepting configurations from different machines.dcc21df : Route rejected promises returned from custom actions to the actor's error handling, so a state's onError catches a failed async action inste
dcc21df: Route rejected promises returned from custom actions to the actor's error handling, so a state's onError catches a failed async action instead of leaving an unhandled rejection. A rejection that was previously ignored now errors the actor when no onError handles it.
const machine = createMachine({
initial: 'active',
states: {
active: {
on: {
SAVE: (_, enq) => {
enq(() => saveToServer()); // returns a Promise
}
},
onError: { target: 'failed' }
},
failed: {}
}
});Add the ErrorFrom type helper. invoke.onError events are typed from the invoked actor's error type when the actor logic declares one.
Add an optional passive flag to Observer. A passive observer only tracks the actor's lifecycle and does not count as an error handler, so an unhandled actor error is still reported when every observer with an error callback is passive.
An unhandled actor error is now reported one macrotask later than before, and a subscriber with an error callback that attaches in that window takes the error instead. Tests that advance fake timers by a single tick to observe the report need one more tick.
8b0d3e7 : State and transition metadata can now use separate schemas:
8b0d3e7: State and transition metadata can now use separate schemas:
const machine = createMachine({
schemas: {
meta: z.object({ label: z.string() }),
transitionMeta: z.object({ trackingId: z.number() })
},
meta: { label: 'Root' },
on: {
NEXT: { meta: { trackingId: 42 } }
}
});When transitionMeta is omitted, schemas.meta continues to apply its type to
both state and transition metadata.
384e5d6: actor.getPersistedSnapshot() is now assignable to PersistedSnapshotFrom<typeof machine>, so persisted snapshots can be annotated with the public type instead of ReturnType<Actor<typeof machine>['getPersistedSnapshot']>:
import { createActor, type PersistedSnapshotFrom } from 'xstate';
const snapshot: PersistedSnapshotFrom<typeof machine> =
createActor(machine).getPersistedSnapshot();
snapshot.context; // typed from the machineEvent executors passed to path.test() from xstate/graph now receive the full event, payload included, instead of just { type }:
await path.test({
events: {
// `event.card` used to require an `Extract<...>` cast
pay: ({ event }) => ui.pay(event.card)
}
});cd98aed: Preserve a state's existing input when a transition targets that state without
reentering it. Reentering transitions continue to replace the state input.
2044d05: Send events to statically declared children by id. Events are checked against
the actor-ref protocol declared in schemas.children.
createMachine({
schemas: {
children: {
worker: types<ActorRefFromLogic<typeof workerLogic>>()
}
},
on: {
notify: (_, enq) => {
enq.sendTo('worker', { type: 'notify' });
}
}
});010298e : Named guards are now plain predicate functions. A guard receives only the arguments you pass it — the transition args object is no longer in
010298e: Named guards are now plain predicate functions. A guard receives only the arguments you pass it — the transition args object is no longer injected first. Pass values from context or the event explicitly:
const machine = createMachine({
context: { count: 0 },
guards: {
// Previously: isAbove: (args, threshold) => args.context.count > threshold
isAbove: (count: number, threshold: number) => count > threshold,
isEnabled: () => true
},
initial: 'a',
states: {
a: {
on: {
NEXT: ({ context, guards }) => {
if (guards.isAbove(context.count, 3) && guards.isEnabled()) {
return { target: 'b' };
}
}
}
},
b: {}
}
});Exception: guards referenced declaratively from serialized JSON or SCXML machines (guard: { type, params }) are invoked by the runtime and still receive the transition args object first, then params — the runtime is the caller there and has nothing else to pass.
c405428: Fixed framework adapter snapshot inference in projects that enable exactOptionalPropertyTypes.
const actor = createActor(machine);
const count = useSelector(actor, (snapshot) => snapshot.context.count);069aadf : Fixed delayed transitions and other built-in effects in minified bundles, preserved hook-owned actors across React StrictMode effect reconne
29dae8c : Improve setup(...) state contracts with typed structural metadata, recursive state input requirements for composite and parallel entry, hist
29dae8c: Improve setup(...) state contracts with typed structural metadata, recursive
state input requirements for composite and parallel entry, history-default
validation, strongly typed relative targets, and shared input requirements for
literal target sets.
b44d9cb: State-level context schemas now refine the root context schema instead of
replacing it. Declare only the fields narrowed by a state while retaining all
root context fields in state actions, transitions, and narrowed snapshots.
Nested states retain active ancestor refinements, and xstate/fsm uses the
same refinement semantics.
const machine = setup({
schemas: {
context: z.object({
requestId: z.string(),
draft: z.string().optional()
})
},
states: {
reviewing: {
schemas: { context: z.object({ draft: z.string() }) }
}
}
}).createMachine({
context: { requestId: 'req-1' },
// ...
});7f9fc4f : Invalid external events are now rejected at the delivery boundary instead of erroring the actor or throwing. This covers events whose payloa
7f9fc4f: Invalid external events are now rejected at the delivery boundary instead of erroring the actor or throwing. This covers events whose payload fails a declared runtime validator schema and internal event types (internalEvents) sent from outside their owning actor. A rejected event is never delivered: the actor does not transition, does not error, and no API throws.
Rejections are reported through:
onRejectedEvent dead-letter hook on createActor(...) options, which receives an EventRejection describing the event, target, source, origin, reason and validation issues;@xstate.deadletter inspection event, which now also carries the validation issues and underlying error for boundary rejections;const actor = createActor(machine, {
onRejectedEvent: (rejection) => {
console.log(rejection.event, rejection.reason, rejection.issues);
}
});Pure transition(...) calls no longer throw for invalid external events: the snapshot is returned unchanged with a @xstate.deadLetter effect carrying the rejection, so durable hosts can journal rejections and replay stays total even with poisoned queued events. The effect executes through the deadLetter runtime operation, so createDurable(...) adapters journal rejections by implementing deadLetter like any other runtime operation.
Internal faults are unchanged: values the machine produces itself (input, context, output, emitted events and delayed raised events) that fail their schema still error the actor, and pure transitions still throw for them.
501c5e3 : Add the self-contained xstate/fsm entry point for compact flat finite state machines. It exports createFSM and createFSMActor without includ
501c5e3: Add the self-contained xstate/fsm entry point for compact flat finite state
machines. It exports createFSM and createFSMActor without including the full
statechart actor runtime.
import { createFSM, createFSMActor } from 'xstate/fsm';
const logic = createFSM({
initial: 'inactive',
states: {
inactive: { on: { toggle: { target: 'active' } } },
active: { on: { toggle: { target: 'inactive' } } }
}
});
const actor = createFSMActor(logic).start();The FSM runtime also supports guarded transitions, context, actions, eventless
transitions, final states, child actors, delayed events, and snapshot
persistence for state, context, and self-directed timers. Live inline children
and timers targeting other actors are rejected at the persistence boundary
because the compact runtime has no registered actor-source registry.
7601447 : Add createMachineFromSCXML(...) through the xstate/scxml entry point. Converted machines follow strict SCXML behavior for transition selecti
7601447: Add createMachineFromSCXML(...) through the xstate/scxml entry point. Converted machines follow strict SCXML behavior for transition selection, executable content, datamodel evaluation, invocation, event metadata, and completion.
import { createMachineFromSCXML } from 'xstate/scxml';
const machine = createMachineFromSCXML(scxml);72938f8 : Guard and delay source functions are now contextually typed from schemas — in setup({ ... }) , .extend({ ... }) , and createMachine({ ... })
72938f8: Guard and delay source functions are now contextually typed from schemas — in setup({ ... }), .extend({ ... }), and createMachine({ ... }) — so inline functions get typed context and event without hand annotations:
const s = setup({
schemas: {
context: z.object({ count: z.number() }),
events: { INC: z.object({ by: z.number() }) }
},
guards: {
// context: { count: number }, event: { type: 'INC'; by: number }
isPositive: ({ context }) => context.count > 0,
// additional params after the args object are free-form
isAbove: ({ context }, threshold: number) => context.count > threshold
},
delays: {
backoff: ({ context }) => context.count * 100
}
});Guard sources receive the transition args object first ({ context, event, self, parent, value, children }), followed by any caller-supplied params — matching how the runtime invokes referenced guards. Delay sources receive { context, event, stateNode }.
Additionally, enq.stop(...), enq.listen(...), and enq.subscribeTo(...) now accept any ActorRef (such as values typed with ActorRefFrom<typeof machine>), instead of requiring the full actor instance type returned by enq.spawn(...).
6ecc2df: Document durable timer semantics for event-journal hosts.
fc7454f: Restoring an externally migrated live snapshot now treats its machine property as a runtime association rather than persisted version metadata. Persisted snapshots continue validating their nested { id, version } identity and legacy top-level version together.
getNextTransitions(snapshot) returns an empty array for completed or errored snapshots.
Setup-created machines whose input schema accepts undefined no longer require a meaningless input property when other actor options are provided, including after machine.provide(...).
Durable adapters can implement enqueueRootEvent when the host owns only the execution root's mailbox, without overriding delivery for co-located actors:
const durable = createDurable(machine, {
enqueueRootEvent: (_source, event) => host.enqueue(event),
executeAction,
waitForEvent
});Implement sendEvent only when the host owns routing for every target; use deliverEvent for co-located delivery. Durable replay guidance now explicitly covers inline entry and exit callbacks.
30eb784 : Transition spawning accepts typed registered actor names so durable source identity can be explicit:
30eb784: Transition spawning accepts typed registered actor names so durable source identity can be explicit:
const machine = createMachine({
actors: { worker },
on: {
start: (_, enq) => {
enq.spawn('worker', { id: 'worker' });
}
}
});The name determines required input and the returned actor reference type. It is resolved immediately and persisted exactly, so duplicate names may share one logic value and later diverge safely. Logic-value spawning remains supported and uses the first matching registered key; unregistered inline children cannot be persisted.
e0dc812 : Durable execution DX improvements:
e0dc812: Durable execution DX improvements:
The drive loop no longer routes root events by hand. executeEffects retains the root-addressed events it captures, and execution.waitForEvent() hands them out before deferring to the adapter, so the canonical loop is:
let [state, effects] = execution.initialTransition(input);
await execution.executeEffects(effects);
while (state.status === 'active') {
[state, effects] = execution.transition(state, await execution.waitForEvent());
await execution.executeEffects(effects);
}(executeEffects now resolves with void.)
createDurable(logic, adapter, { inspect }) observes the execution's inspection events (@xstate.actor / @xstate.transition) across the whole live actor tree, including transitions computed by the pure path — the host-side home for operation logs and instrumentation.
execution.getActorRef(snapshot, address) resolves a logical address against the snapshot's live actor tree, for hosts whose durable mailbox stores addresses as strings.
Machine output types infer from the config's output function when no schemas.output schema is declared; a declared schema stays authoritative.
DurableSnapshot keeps the status/output/error discriminant visible when TLogic is an unresolved type parameter, and adapter waitForEvent implementations may return plain event objects — generic host libraries no longer need casts.
e0dc812: A machine's output type is now inferred from its output config when no schemas.output is declared. A declared schemas.output stays authoritative.
const machine = setup({}).createMachine({
context: { shipped: ['sku-1'] },
initial: 'done',
states: { done: { type: 'final' } },
output: ({ context }) => ({
status: 'shipped' as const,
skus: context.shipped
})
});
// OutputFrom<typeof machine> is now
// { status: 'shipped'; skus: string[] } instead of {}7156ad5 : Add setup state-local output schemas for typed final-state output and nested completion contracts.
39f8431 : Durable executions can pin a deterministic identity: createDurable(machine, { executionId, ... }) makes session ids <executionId>:<n> , a de
39f8431: Durable executions can pin a deterministic identity: createDurable(machine, { executionId, ... }) makes session ids <executionId>:<n>, a deterministic function of actor-creation order, so hosts that journal the execution's own events can replay them — a journaled completion event still matches the child a replay re-creates.
const durable = createDurable(machine, {
executionId: orderId,
executeAction,
waitForEvent
});The new runLogic runtime operation makes the invoked async actor the primary durable unit: a developer writes a normal promise and the host journals the whole body as one entry — wrapping the provided thunk in its own step primitive, or re-running the registered logic on a remote executor from the actor's serializable (src, input) identity alone. enq.step remains for opt-in finer granularity.
runLogic: (actor, exec) => ctx.run(actor.address, exec)A remote handle now carries its host-supplied incarnation token as its sessionId — one incarnation identity with one staleness rule (a ref that knows its incarnation compares it; one that does not defers to the owning runtime). The persisted incarnation field is unchanged.
Docs reposition enq.step as the hostless-durability tool (the snapshot is the journal; durable hosts use plain promise actors + runLogic), document the portable-action rule (named function + serializable args ships to a remote executor; closures run in-process), and add a "Driving effects yourself" section: createDurable is sugar over the pure transition() APIs, and hosts may execute the effect objects themselves.
Durable-execution docs now also cover quiescing in-flight steps before registering a durable wait, the ordered-effect replay guarantee, and that runStep's exec closure must run in-process.
14cfdc3 : Actors now have deterministic, location-transparent identity.
14cfdc3: Actors now have deterministic, location-transparent identity.
address: the /-joined path of actor ids from the root. Root actors are named after their logic's id, and children spawned without an explicit id get deterministic per-parent counters keyed by their actor source (worker:0, worker:1). Addresses are stable across persistence and restore; sessionId identifies one incarnation of an address, and completions from a previous incarnation of a local child are dropped.enq.spawn(actors.x) records the registered source key so spawned children persist by key.getEffectDescriptor(effect) returns a serializable view of any executable effect, with actor references replaced by addresses and actor sources by source keys (payload fields pass through by reference).system.runtime; the built-in local runtime is the default. The new deliverEvent, stopActor and terminateActor helpers expose the local behaviors for custom runtimes to delegate to.createDurable (from xstate/durable) adapters carry their runtime operations directly (sendEvent, scheduleTimer, …), and the execution installs them on every snapshot's actor system; it exposes rootAddress and getActorRef(snapshot), tags every effect with a serializable descriptor, and executeEffects resolves only when every transitively initiated runtime operation has been accepted — returning the events addressed to the root actor for the durable loop. Breaking for existing adapters: during executeEffects, root-addressed events no longer reach any runtime sendEvent (including per-effect runtime() implementations) — drain them from the executeEffects result instead. While the loop is parked in waitForEvent, a root-addressed event reaches sendEvent like any other target, and the host should enqueue it in its own mailbox. Runtime objects are also wrapped before effects see them, so identity comparisons and extra non-runtime properties on the returned object are not preserved.getPersistedSnapshot(snapshot, { embedChildren: false }) persists children by logical address, leaving each child's state with the runtime that owns it; restoring an address-only child produces a location-transparent handle whose sends route through the system runtime.deadLetter runtime operation and a @xstate.deadletter inspection event. Delivery stays at-most-once — this is observability, not retry.incarnation token. XState never stamps one, but when a host does, completions from a different incarnation of the address are dropped and sendTo effect descriptors journal the target's token.createDurable exposes machineId and machineVersion so hosts can pin an execution's journal to the machine version that produced it.enq.step) route through the new runStep runtime operation. The built-in behavior memoizes results in the actor's own snapshot as before; a durable host implements runStep to own the step journal, replaying memoized results without re-running the step. The runStep helper export exposes the built-in behavior.Actor.toJSON, persisted context refs) carry xstate$type: 'actorRef' instead of the v5 xstate$$type: 1 marker. Migrate v5-persisted context refs with machineVersions if you restore them.startedAt); restoring the snapshot schedules the remaining time toward the original deadline (clamped to the declared delay), so a timer past due fires immediately instead of restarting its full delay. Pure-transition snapshots carry no timestamp and restart the declared delay.const durable = createDurable(machine, {
sendEvent: (source, target, event) =>
host.send(source?.address, target.address, event),
executeAction: (action, { id }, runtime) =>
host.runAction(id, () => action.exec(runtime)),
waitForEvent: ({ id }) => host.waitForEvent(id)
});107d0c2 : Represent historical machine versions with Standard Schema snapshot and event descriptors instead of retaining their executable machines. Ve
107d0c2: Represent historical machine versions with Standard Schema snapshot and event
descriptors instead of retaining their executable machines. Versioned machines
expose the same snapshotSchema and eventSchema interface. Machine-backed
snapshot schemas default omitted history and timer records during migration.
const versions = machineVersions([
{
id: 'checkout',
version: '1',
snapshotSchema: checkoutV1Snapshot,
eventSchema: checkoutV1Event
},
checkoutV2
]);
const snapshot = await versions.migrateSnapshot(persisted, {
to: '2',
migrations: {
'1': (snapshot) => ({
...snapshot,
context: { total: snapshot.context.count }
})
}
});2e77ff6: actor.getPersistedSnapshot() is now typed to the actor’s logic, so persist/restore round-trips through createActor(logic, { snapshot }) no longer require a cast. Restoring a snapshot into a machine with a different id is a type error, while snapshots from other versions of the same machine remain assignable (for runtime migration via migrate).
const actor = createActor(machine).start();
const snapshot = actor.getPersistedSnapshot();
// No cast needed:
const restored = createActor(machine, { snapshot }).start();7a7e564 : Fixed a bug where enq.subscribeTo(…) and enq.listen(…) silently did nothing when called inside a transition function. They now work the same
7a7e564: Fixed a bug where enq.subscribeTo(…) and enq.listen(…) silently did nothing when called inside a transition function. They now work the same as in entry actions:
const machine = createMachine({
on: {
start: (_, enq) => {
const child = enq.spawn(childLogic);
enq.subscribeTo(child, {
done: (output) => ({ type: 'childDone', output })
});
},
childDone: ({ event }) => {
// event.output is the child's output
}
}
});4e6dbfd: Named imports from the root xstate entry (such as the actor logic creators and SpecialTargets) now work in all environments, including tools that load the package as CommonJS.
import { createAsyncLogic } from 'xstate'; // now works everywhere20a52b9: Optional event payload fields declared with types() are now preserved instead of being made required:
const machine = setup({
schemas: {
events: {
submit: types<{ email: string; referrer?: string }>()
}
}
}).createMachine({
// ...
});
// referrer can now be omitted
actor.send({ type: 'submit', email: 'a@b.co' });Payloads inferred from validator libraries (e.g. Zod) are still wrapped in Required<>.
20a52b9: snapshot.value is now structurally typed from the machine config instead of the generic StateValue type. Flat machines get a union of state keys, and parallel machines get an object type with a key per region:
const machine = createMachine({
type: 'parallel',
states: {
bold: { initial: 'off', states: { off: {}, on: {} } },
italic: { initial: 'off', states: { off: {}, on: {} } }
}
});
const value = createActor(machine).getSnapshot().value;
// { bold: 'off' | 'on'; italic: 'off' | 'on' }90a173a: Fixed createSystem(...).setup(...) to preserve runtime validator types alongside typed actor registries. Validated setups now reject unsupported transforming schemas and carry validation into derived setups.
Runtime validation can now also be installed on a derived setup when its inherited schemas are compatible:
import { setup } from 'xstate';
import { standardSchemaValidator } from 'xstate/validation';
import { z } from 'zod';
const validated = setup({
schemas: { input: z.object({ id: z.string() }) }
}).extend({ validator: standardSchemaValidator() });8aaac46 : Restoring a persisted snapshot whose state value references a state that no longer exists on the machine now throws a descriptive error, e.g
8aaac46: Restoring a persisted snapshot whose state value references a state that no longer exists on the machine now throws a descriptive error, e.g.:
Persisted snapshot references state 'reviewing' which does not exist on machine 'order-approval'.
Nested state values report the full state path (e.g. 'active.reviewing'), and parallel state regions are validated as well.
86f7303: Add an experimental, host-neutral durable execution helper at xstate/durable. It assigns stable IDs to effects and event waits from pure tran
86f7303: Add an experimental, host-neutral durable execution helper at xstate/durable.
It assigns stable IDs to effects and event waits from pure transitions while
leaving durable execution, timers, messaging and child actors to the host.
Hosts can read nextTransitionIndex after every transition for checkpointing.
const durable = createDurable(machine, adapter);
const output = await durable.run(input);
86f7303: Add experimental durable execution adapters for Inngest and Rivet workflows. Host runtime mappings now receive the complete built-in effect, allowing them to map timers, sends and child actors without coupling XState to either host.
import { createDurable } from '@xstate/inngest';
const output = await createDurable(machine, options).run(input);
6df07b8: Fixed invoke.onDone transition argument inference when actor logic is passed directly as src in a machine with two or more differently-typed registered actors. Previously, the transition function's arguments collapsed to any (event.output was unusable without annotations); now event.output is inferred from the invoked actor's output type.
const fetchUser = createAsyncLogic({ run: async () => ({ name: 'David' }) });
const fetchCount = createAsyncLogic({ run: async () => 42 });
setup({
actors: { fetchUser, fetchCount }
}).createMachine({
initial: 'loading',
states: {
loading: {
invoke: {
src: fetchUser,
onDone: ({ event }) => {
event.output.name; // string
return { target: 'done' };
}
}
},
done: {}
}
});
Note: when actors are registered, invoking unregistered inline logic now only supports the object/target forms of onDone (not the transition function form). Register the actor to get fully-typed function-form transitions.
af31f22: Add machineVersions().adaptEvents() for adapting complete event histories between machine versions. Exact retained-version adapters infer sou
af31f22: Add machineVersions().adaptEvents() for adapting complete event histories
between machine versions. Exact retained-version adapters infer source and
target event types, while an async '*' adapter can handle unknown histories.
const events = await versions.adaptEvents(storedEvents, {
from: { id: 'checkout', version: '1' },
to: '2',
adapters: {
'1': (events) => events.map(toV2Event)
}
});
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
e410f24: Add state-level onError transitions for handling xstate.error.* events.
e410f24: Add state-level onError transitions for handling xstate.error.* events.
State onError catches actor, execution, and communication errors while the state is active. The caught error is available on event.error.
const machine = createMachine({
initial: 'active',
states: {
active: {
onError: ({ event }) => ({
target: 'failed',
context: {
message:
event.error instanceof Error
? event.error.message
: String(event.error)
}
})
},
failed: {}
}
});
f6edec7: Allow machines with no external events to be used anywhere AnyActorLogic or AnyStateMachine is expected.
const machine = setup({
schemas: {
events: {}
}
}).createMachine({});
const logic: AnyActorLogic = machine;
const anyMachine: AnyStateMachine = machine;
Machines with empty event schemas still reject external events sent to their actors.
37d3254: Setup-bound invoke transition callbacks now validate target state context requirements for onDone, onError, onSnapshot, and onTimeout. onDone
37d3254: Setup-bound invoke transition callbacks now validate target state context requirements for onDone, onError, onSnapshot, and onTimeout. onDone also infers output from the invoked actor logic.
import { createAsyncLogic, setup } from 'xstate';
import { z } from 'zod';
const machine = setup({
actorSources: {
loadUser: createAsyncLogic({
run: async () => ({ name: 'Ada' })
})
},
states: {
loading: {},
success: {
schemas: {
context: z.object({
user: z.object({ name: z.string() })
})
}
}
}
}).createMachine({
context: {},
initial: 'loading',
states: {
loading: {
invoke: {
src: 'loadUser',
// Type-safe return value for invoke callbacks
onDone: ({ event }) => ({
target: 'success',
context: { user: event.output }
})
}
},
success: {}
}
});
c0c21d0: Add a path-bound overload to setup(...).createStateConfig(path, config).
c0c21d0: Add a path-bound overload to setup(...).createStateConfig(path, config).
When a state declares its own input schema, the anonymous createStateConfig(config) form types input as a broad union across all states. This makes the resulting config incompatible with the specific state it's meant for — assigning it inside createMachine produces a type error because the input types don't match.
The new createStateConfig(path, config) overload binds the config to a specific setup-declared state by dotted path (e.g. 'loading' or 'parent.child'). The addressed state's own input schema is used inside entry/exit args, and bare transition targets are validated against the state's siblings.
const s = setup({
states: {
idle: {},
active: {
schemas: { input: z.object({ userId: z.string() }) }
}
}
});
// Before: anonymous form — `input` is typed broadly, and assigning this
// config to the `active` state in createMachine fails with a type error.
const active = s.createStateConfig({
entry: ({ input }) => {
// input is not narrowed to { userId: string }
}
});
// After: path-bound form — `input` is narrowed to `active`'s own schema.
const active = s.createStateConfig('active', {
entry: ({ input }) => {
input.userId; // string
}
});
// Works for nested states too:
const child = s.createStateConfig('parent.child', { ... });
8e3cce6: Fix snapshot.matches(...) narrowing so repeated checks like snapshot.matches('loaded') || snapshot.matches('failed') compile correctly, and make StateFrom<typeof machine> preserve the machine's concrete state value.
0c2a6e5: Fix function-syntax transitions not passing input to target state entry actions.
on: {
FETCH: ({ context, event }) => ({
target: 'fetching',
input: { url: event.url, token: context.authToken }
});
}
Previously, input returned from function-syntax transitions was silently ignored. Now it is correctly forwarded to the target state's entry action.
bdc54dd: Added createFSM(...) for flat, actor-compatible finite state machines.
bdc54dd: Added createFSM(...) for flat, actor-compatible finite state machines.
import { createActor, createFSM } from 'xstate';
const toggleLogic = createFSM({
initial: 'inactive',
context: { count: 0 },
states: {
inactive: {
on: {
toggle: {
target: 'active',
context: { count: 1 }
}
}
},
active: {
on: {
toggle: ({ context }, enq) => {
enq(() => console.log('toggled'));
return {
target: 'inactive',
context: { count: context.count + 1 }
};
}
}
}
}
});
const actor = createActor(toggleLogic).start();
actor.send({ type: 'toggle' });
createFSM(...) supports XState-style object transitions, function transitions, enq actions, initial input, state input, entry actions, and exit actions. Plain string targets are intentionally not supported; use object targets such as { target: 'active' }.
Simple FSM transitions preserve immutable public snapshots while using structural sharing and a lighter transition path for common { target }, { context }, and { target, context } transitions.
e297115: Object transition configs now support dynamic context patches with a context mapper.
onDone: {
target: 'done',
context: ({ context, output }) => ({
answer: output,
memory: [...context.memory, output]
})
}
4d9ba1c: Spawning a child with enq.spawn(...) from a transition function now creates and starts the child actor exactly once for the committed transit
4d9ba1c: Spawning a child with enq.spawn(...) from a transition function now creates and starts the child actor exactly once for the committed transition.
const machine = createMachine({
on: {
spawn: (_, enq) => {
enq.spawn(childMachine, { registryKey: 'child' });
}
}
});
6798cb1: serializeMachine(...) and createMachineFromConfig(...) now represent inline functions (guards, actions, transitions, delays, route functions) as { '@code': string, '@lang': 'ts' }. Non-portable values such as actor logic, runtime schemas, class instances, symbols, and bigints are omitted from the serialized JSON.
import { serializeMachine } from 'xstate';
serializeMachine(machine);
// inline functions → { '@code': '() => true', '@lang': 'ts' }
Type-only refinements: void and undefined are accepted as type-only schemas, async logic output is inferred from an input-only schema, actions/guards can be typed via schemas, and trigger is correctly typed on spawned actors.
57e8d85: Machines that declare schemas.output now type-check top-level final state output values against the machine output type.
57e8d85: Machines that declare schemas.output now type-check top-level final state
output values against the machine output type.
createMachine({
schemas: {
output: types<{ status: 'ok' }>()
},
initial: 'done',
states: {
done: {
type: 'final',
output: { status: 'ok' }
}
},
output: ({ event }) => event.output
});
86b43ea: Export setup system helper types used by public machine types.
86b43ea: Export setup system helper types used by public machine types.
This avoids inferred machine types referring to internal declaration paths when
setup(...) includes a typed system registry.
54205cc: Export setup helper types for libraries that return or decorate setup-bound objects while preserving native setup(...).createMachine(...) typ
54205cc: Export setup helper types for libraries that return or decorate setup-bound objects while preserving native setup(...).createMachine(...) typing.
import {
setup,
type AnySetupConfig,
type SetupReturnFromConfig
} from 'xstate';
function decorateSetup<const TConfig extends AnySetupConfig>(
config: TConfig
): SetupReturnFromConfig<TConfig> & { extra: true } {
const s = setup(config) as SetupReturnFromConfig<TConfig>;
return Object.assign(s, { extra: true as const });
}
{ type: 'SEND' } is accepted for an empty SEND payload schema while non-empty schemas still require their payload fields.onDone callbacks now receive the invoked actor's output type, and machine.provide({ actorSources }) accepts compatible actor implementations with sound input/output variance.667d1c7: Done transitions now receive output directly in callback arguments.
667d1c7: Done transitions now receive output directly in callback arguments.
invoke: {
src: fetchUser,
onDone: ({ output }) => {
output.name;
}
}
The direct output value is only provided for XState done events, such as
xstate.done.actor.* and xstate.done.state.*.
89895f9: Add createSystem({ registry }) for declaring typed actor registry keys and creating actors in that system.
89895f9: Add createSystem({ registry }) for declaring typed actor registry keys and creating actors in that system.
Registry keys are assigned with registryKey on invokes, spawned actors, and root actors created from the system. Registry keys are checked against the declared registry when using createSystem.
const system = createSystem({
registry: {
receiver: receiverLogic
}
});
const machine = system.setup().createMachine({
invoke: {
src: receiverLogic,
registryKey: 'receiver'
}
});
const actor = system.createActor(machine).start();
system.get('receiver')?.send({ type: 'HELLO' });
89895f9: Static transition config objects may now include a shallow context patch.
createMachine({
context: { draftAnyway: false, count: 0 },
initial: 'idle',
states: {
idle: {
on: {
DRAFT_ANYWAY: {
target: 'drafting',
context: { draftAnyway: true }
}
}
},
drafting: {}
}
});
The patch is shallow-merged with the current context, just like context returned from a transition function. Setup-typed machines still require any keys needed by the target state's narrowed context.
ecd97db: State transition functions now type enq with the machine's events and emitted events.
ecd97db: State transition functions now type enq with the machine's events and emitted events.
setup({
schemas: {
events: {
go: types<{}>()
}
}
}).createMachine({
states: {
active: {
on: {
go: (_args, enq) => {
enq.raise({ type: 'go' });
}
}
}
}
});
4b5b14f: Transition functions may now return only a target when the target state's context is compatible with the current context.
setup({
schemas: {
context: types<{ count: number }>(),
events: {
next: types<{}>()
}
}
}).createMachine({
context: { count: 0 },
initial: 'idle',
states: {
idle: {
on: {
next: () => ({ target: 'done' })
}
},
done: {}
}
});
297f851: String target shorthand is no longer accepted for transition configs. Use the object form with target instead:
297f851: String target shorthand is no longer accepted for transition configs. Use the object form with target instead:
createMachine({
initial: 'idle',
states: {
idle: {
on: {
start: { target: 'active' }
}
},
active: {}
}
});
c3f7a9d: Actor logic now returns effects from both regular and initial transitions.
c3f7a9d: Actor logic now returns effects from both regular and initial transitions.
Hand-written actor logic should return [snapshot, effects] from transition(...) and provide initialTransition(...) for creating the initial [snapshot, effects] tuple. getInitialSnapshot(...) remains available for snapshot-only reads.
const logic = {
transition: (snapshot, event) => [snapshot, []],
initialTransition: (input, _scope) => [
{
status: 'active',
output: undefined,
error: undefined,
input
},
[]
],
getInitialSnapshot: (scope, input) =>
logic.initialTransition(input, scope)[0]
};
transition(...) and initialTransition(...) continue to return [snapshot, actions] for machine logic.
fromStore(...) effects now run after the actor snapshot is committed, so effect callbacks read the updated snapshot from enqueue.getSnapshot().
309b106: Add schemas.children for explicitly typing child actor refs by child ID. Declared child refs type children.someId, child snapshots, and invoke configs so invoke: { id: 'someId', src } must match the declared child actor contract.
fa2bbf0: Built-in executable effects returned from transition(...) and initialTransition(...) are now easier to inspect declaratively.
Use isBuiltInExecutableAction(effect) to narrow an executable effect to XState's built-in effect union, then switch on effect.type to access stable, named metadata fields:
const [snapshot, effects] = initialTransition(machine);
for (const effect of effects) {
if (!isBuiltInExecutableAction(effect)) {
continue;
}
switch (effect.type) {
case '@xstate.start':
effect.id;
effect.logic;
effect.src;
effect.input;
break;
case '@xstate.sendTo':
effect.target;
effect.event;
effect.delay;
break;
case '@xstate.raise':
effect.event;
effect.delay;
break;
}
}
The built-in stop effect is now exposed as @xstate.stop, matching @xstate.start.
```ts const machine = createMachine({ schemas: { context: z.object({ count: z.number() }), events: { inc: z.object({ by: z.number() }) } }, context: {
#44 0a883ad Thanks @pull! - Expose machine.schemas as a public runtime-readable schema contract.
const machine = createMachine({
schemas: {
context: z.object({ count: z.number() }),
events: {
inc: z.object({ by: z.number() })
}
},
context: { count: 0 }
});
machine.schemas?.events?.inc;
#44 d95287b Thanks @pull! - Serialize function implementations as portable code expressions.
Functions in guards, actions, delays, and inline machine config now serialize as:
{ '@type': 'code', lang: 'ts', expr: '() => true' }
Values that cannot be represented as code or JSON still serialize with explicit $unserializable markers.
useActor, useActorRef and useMachine now require input for machines that declare a required input schema, matching createActor. Passing a persisted snapshot instead of input is allowed.useActor, useActorRef and useMachine now require input for machines that declare a required input schema, matching createActor. Passing a persisted snapshot instead of input is allowed.@xstate/svelte as an ES module so import resolves to real ESM instead of a CommonJS wrapper. This fixes Vitest and other ESM tooling failing with require('svelte/store') errors against Svelte's ESM-only builds. require('@xstate/svelte') still works on Node versions that support require(esm).`ts const machine = createMachine({ context: { value: 0 }, initial: 'idle', states: { idle: { on: { start: () => ({ target: 'active', context: { value
#44 d6a537e Thanks @pull! - Fixed a bug where an invoked actor's input (and a dynamic src function) received the context from before the transition that entered the invoking state, rather than the updated context. Now, when a transition updates context and targets a state that invokes an actor, the actor's input sees the updated context — consistent with that state's entry actions.
const machine = createMachine({
context: { value: 0 },
initial: 'idle',
states: {
idle: {
on: {
start: () => ({ target: 'active', context: { value: 100 } })
}
},
active: {
invoke: {
src: asyncLogic,
// now receives { value: 100 } instead of { value: 0 }
input: ({ context }) => ({ val: context.value })
}
}
}
});
69b6663: Keep selected values current when subscriptions start or resume. Vue and Solid now expose terminal error snapshots, and Solid preserves array/object context changes without changing the actor's source data. Vue uses one snapshot subscription per useActor call.
React store selectors now honor custom comparisons when the selector argument is omitted.
The user-visible consequence: a child that fails synchronously while starting now surfaces that failure through the invoking state's onError transitio
#44 52970ea Thanks @pull! - Invoked and spawned actors are no longer started directly by actor.start(). They now start as part of the transition that creates them (via an internal deferred start action), the same way other entry effects run.
The user-visible consequence: a child that fails synchronously while starting now surfaces that failure through the invoking state's onError transition instead of throwing out of actor.start():
const machine = createMachine({
initial: 'loading',
states: {
loading: {
invoke: {
src: createAsyncLogic({
run: () => {
throw new Error('boom'); // sync failure on start
}
}),
onError: 'failed'
}
},
failed: {}
}
});
const actor = createActor(machine).start(); // does not throw
actor.getSnapshot().value; // 'failed'
Restored (rehydrated) children that were active when a snapshot was persisted are still restarted on actor.start(), so persistence behavior is unchanged.
#44 52970ea Thanks @pull! - Actions, guards, and transitions are now plain inline functions, and the v5 action/guard creators are removed.
Removed exports: assign, raise, sendTo, sendParent, forwardTo, emit, log, cancel, spawnChild, stop, stopChild, enqueueActions, and the guard creators and, or, not, stateIn.
Instead, a transition/action/guard is a function (args, enq) => ...:
{ context } patch (no more assign).enq enqueue object: enq.raise, enq.sendTo, enq.emit, enq.log, enq.cancel, enq.spawn, enq.stop, plus enq(fn, ...args) for arbitrary effects.undefined/false to block).- import { assign, raise, sendTo, and, not } from 'xstate';
const machine = createMachine({
context: { count: 0 },
on: {
- INC: {
- guard: and([not('isMax'), 'isReady']),
- actions: assign({ count: ({ context }) => context.count + 1 })
- }
+ INC: ({ context, guards }) => {
+ if (guards.isMax(context) || !guards.isReady(context)) return;
+ return { context: { count: context.count + 1 } };
+ }
}
});
The stateIn guard is replaced by checking the snapshot directly — use snapshot.matches(...) inside a transition function:
on: {
CHECK: ({ self }) => {
if (self.getSnapshot().matches({ b: 'b2' })) {
return { target: 'a2' };
}
};
}
For matching by state id (the '#id' form, which matches() doesn't resolve), the exported checkStateIn(snapshot, '#id') helper is also available.
#44 52970ea Thanks @pull! - Remove the deprecated interpret function and Interpreter type. Use createActor(...) and Actor (or ActorRefFrom<...>) instead.
- import { interpret, type Interpreter } from 'xstate';
- const actor = interpret(machine);
+ import { createActor, type Actor } from 'xstate';
+ const actor = createActor(machine);
#44 52970ea Thanks @pull! - schemas is now the way to type a machine, replacing v5's types: {} as {...}. Each schemas field accepts any Standard Schema (Zod, Valibot, …) for both type inference and (where supported) runtime validation, or types<T>() for types only.
Notably, schemas.events is a map of event-type → payload schema, inferred into a discriminated union keyed by type:
import { createMachine } from 'xstate';
import { z } from 'zod';
const machine = createMachine({
schemas: {
context: z.object({ count: z.number() }),
events: {
inc: z.object({ by: z.number() }),
reset: z.object({})
},
input: z.object({ start: z.number() }),
output: z.object({ total: z.number() }),
emitted: { changed: z.object({ count: z.number() }) },
tags: z.union([z.literal('busy'), z.literal('idle')]),
meta: z.object({ label: z.string() })
},
context: ({ input }) => ({ count: input.start }),
initial: 'active',
states: {
active: {
on: {
inc: ({ context, event }) => ({
context: { count: context.count + event.by }
})
}
}
}
});
context → context type (literal initial values are widened, so updates typecheck).events → { type: 'inc'; by: number } | { type: 'reset' }; payloads are typed on event in every transition/action/guard function.input → typed createActor(machine, { input }) and the context initializer argument.output → typed snapshot.output.emitted → typed actor.on('changed', (ev) => ev.count).tags → constrains snapshot.hasTag(...).meta → typed state meta.actors, actions, guards, and delays are top-level config keys (now inline functions), not schemas keys.
#44 96aee67 Thanks @pull! - Separate concrete actors from actor refs in public types. ActorRef now represents the consumer-facing contract for sending events, reading published snapshots, and listening to emitted events with actorRef.on(...); concrete Actor instances provide lifecycle and runtime capabilities and still satisfy actor ref contracts.
#44 52970ea Thanks @pull! - setup(...) no longer registers implementations. It now takes only { schemas?, states? } and returns { createMachine, createStateConfig, states }.
In v5, setup({ schemas, actors, actions, guards, delays }) registered named implementations and returned action creators (assign, sendTo, raise, …). In v6, actions/guards/actors/delays are plain inline functions, so setup no longer accepts or returns them. Its job is now machine- and state-level typing: it validates state keys, initial, and transition targets against the declared states, and types per-state input/context.
const { createMachine, createStateConfig } = setup({
schemas: {
context: types<{ count: number }>(),
events: { INC: types<{ value: number }>() }
},
states: {
idle: {},
loading: { schemas: { input: z.object({ userId: z.string() }) } }
}
});
setup().createMachine() merges setup schemas with config schemas. Bare createMachine({ schemas }) infers the same machine-level types without the state-key checks.
#44 52970ea Thanks @pull! - Add actor.trigger — a typed event-sender proxy. actor.trigger.EVENT(payload) is shorthand for actor.send({ type: 'EVENT', ...payload }):
actor.trigger.NEXT();
actor.trigger.INC({ by: 5 });
#44 021cc56 Thanks @pull! - Machine JSON revival now preserves more of the serialized machine definition, including delayed transitions, state timeouts, state tags, state output, invoke input, invoke completion transitions, invoke timeouts, and implementation maps passed to createMachineFromConfig.
const machine = createMachineFromConfig(
{
initial: 'loading',
states: {
loading: {
invoke: {
src: 'loadUser',
input: { userId: '42' },
onDone: { target: 'done' },
timeout: 5000,
onTimeout: { target: 'timedOut' }
}
},
done: {},
timedOut: {}
}
},
{
actors: { loadUser }
}
);
The migration codemod now reports manual review notes for known non-rename migrations such as fromPromise(...), return assign(...), object-form actions/guards, and legacy types: {} schema declarations.
#44 52970ea Thanks @pull! - createLogic and createAsyncLogic gain a durable-effect enqueue API on their run function's second argument (enq).
enq.effect(key?, fn) registers a side effect that runs once per key (an unnamed effect runs every transition) and is cleaned up when the actor stops.enq.step(key, asyncFn) (async logic) is an await-able step whose result is memoized into the persisted snapshot under snapshot.effects[key]. A rehydrated actor replays run but skips steps that already completed, so long-running async logic is resumable across persistence.const logic = createAsyncLogic({
run: async (_, enq) => {
const user = await enq.step('fetchUser', () => fetchUser());
const order = await enq.step('createOrder', () => createOrder(user.id));
return order.id;
}
});
// snapshot.effects.fetchUser === { status: 'done', output: { id: 1 } }
A pending step can also be resolved externally by sending { type: 'xstate.logic.effect.resolve', key, output }. The LogicEnqueue, LogicEffect, and LogicEffectState types are exported.
#44 52970ea Thanks @pull! - Add timeouts and duration-string delays.
State-level timeout / onTimeout — declare a timeout on a state that transitions when the duration elapses (and is cancelled if the state is exited first):
states: {
waiting: {
timeout: 1000,
onTimeout: 'escalated'
},
escalated: {}
}
createAsyncLogic timeout — async logic can time out; when it does, the run's AbortSignal is aborted and the actor errors with a TimeoutError (exported from xstate):
const logic = createAsyncLogic({
timeout: '10ms',
run: ({ signal }) => fetch('/slow', { signal })
});
Invoke-level timeout / onTimeout — an invocation can race a timeout: if the invoked actor doesn't complete in time, the onTimeout transition is taken; if it settles first (or the state is exited), the timeout is cancelled. timeout accepts a number, a duration string, a referenced delay, or a function ({ context, event }) => duration. Both state- and invoke-level timeout throw at construction if declared without a matching onTimeout.
working: {
invoke: {
src: fetchReport,
timeout: ({ context }) => context.slaMs,
onTimeout: 'timedOut',
onDone: 'done'
}
}
Duration-string delays — delays (including after and timeout) accept human-readable strings like '10ms' and '5s', as well as ISO-8601 durations like 'PT2M', in addition to numbers:
waiting: {
after: {
'5s': 'timedOut'
}
}
#44 d9079cd Thanks @pull! - Logic creators now accept Standard Schemas for type inference.
createLogic(...) and createAsyncLogic(...) accept schemas.input and
schemas.output:
const loadUser = createAsyncLogic({
schemas: {
input: z.object({ userId: z.string() }),
output: z.object({ name: z.string() })
},
run: async ({ input }) => {
input.userId; // string
return {
name: 'David'
};
}
});
The schemas are type-only for now. Runtime validation will be added later as an opt-in behavior.
createCallbackLogic(...), createObservableLogic(...), and createEventObservableLogic(...) also accept schemas.input with object-form config:
const logic = createCallbackLogic({
schemas: {
input: z.object({ userId: z.string() })
},
run: ({ input }) => {
input.userId; // string
}
});
#44 52970ea Thanks @pull! - Add createStateConfig(...) — author a standalone, fully-typed state node config (with schemas) that can be composed into a machine, mirroring how setup(...).createMachine(...) infers types.
import { createStateConfig } from 'xstate';
const loading = createStateConfig({
on: {
RESOLVE: 'success'
}
});
This is the building block for authoring machines as plain data: a createStateConfig node is a typed, serializable config object you compose into a machine — useful for data-first / JSON-driven state machines (round-tripping with serializeMachine/createMachineFromConfig) while keeping per-state schema typing.
#44 52970ea Thanks @pull! - Add enq.listen and enq.subscribeTo for subscribing to other actors from inside transition/action functions.
enq.listen(ref, eventType, mapper) subscribes to events emitted by another actor (supports wildcards like 'data.*') and relays a mapped event back to the current actor.enq.subscribeTo(ref, mappers) subscribes to another actor's snapshot/done/error (pass { snapshot, done, error }, or a single function as snapshot shorthand). It also accepts an atom, in which case the mapper receives the atom's current value.Both return a stoppable child ref (enq.stop(ref)) and are torn down automatically when the parent stops. The underlying logic creators createListenerLogic and createSubscriptionLogic are exported.
entry: (_, enq) => {
const child = enq.spawn(childLogic, { id: 'child' });
enq.listen(child, 'data.*', (ev) => ({ type: 'DATA', value: ev.value }));
enq.subscribeTo(child, {
done: (output) => ({ type: 'CHILD_DONE', output })
});
};
#44 52970ea Thanks @pull! - Add internalEvents: a list of event types that may be raised from within the machine (e.g. via enq.raise(...)) but are rejected when sent to the actor from the outside.
const machine = createMachine({
internalEvents: ['tick'] as const,
initial: 'idle',
states: {
idle: {
on: {
start: (_, enq) => {
enq.raise({ type: 'tick' }); // allowed internally
},
tick: 'running'
}
},
running: {}
}
});
// actor.send({ type: 'tick' }) from outside is rejected
#44 021cc56 Thanks @pull! - State-level schemas.context now narrows context types for state actions, transition functions, and snapshots checked with snapshot.matches(...).
const machine = setup({
states: {
idle: {
schemas: { context: z.object({ user: z.null() }) }
},
success: {
schemas: { context: z.object({ user: z.string() }) }
}
}
}).createMachine({
schemas: {
context: z.object({ user: z.string().nullable() }),
events: {
LOAD: z.object({})
}
},
initial: 'idle',
context: { user: null },
states: {
idle: {
on: {
LOAD: () => ({
target: 'success',
context: { user: 'Ada' }
})
}
},
success: {
entry: ({ context }) => {
context.user; // string
}
}
}
});
const actor = createActor(machine).start();
const snapshot = actor.getSnapshot();
if (snapshot.matches('success')) {
snapshot.context.user; // string
}
State-level schemas.input is also supported: input supplied on a transition or initial ({ target, input }) is typed in that state's entry/exit and transition functions via ({ input }), read from a snapshot with snapshot.getInputs() (keyed by state node id), and typed recursively for nested states.
#44 52970ea Thanks @pull! - Add choice states — a state that immediately routes to a target via a resolver function, returning the first matching transition config.
const machine = createMachine({
context: { userStatus: 'vip' },
initial: 'routing',
states: {
routing: {
type: 'choice',
choice: ({ context }) => {
if (context.userStatus === 'vip') return { target: 'vipFlow' };
return { target: 'standardFlow' };
}
},
vipFlow: {},
standardFlow: {}
}
});
A choice state must declare a choice function and must resolve to a target, and may not declare entry/exit/on/after/invoke — these throw at construction.
#44 52970ea Thanks @pull! - The send function returned by useMachine (@xstate/vue) and useActor (@xstate/svelte) is now typed as the actor's own send signature, matching actorRef.send's overloads.
Updated dependencies [52970ea, 021cc56, 52970ea, 52970ea, 52970ea, 52970ea, d9079cd, 52970ea, 52970ea, 52970ea, 52970ea, 52970ea, 96aee67, 52970ea, 021cc56, 52970ea]:
Your coding agent can read these notes before it upgrades. Set up the MCP server →