xstate-v5 (Strict) Skill
Build, refactor, and review XState v5 state machines (TypeScript and React) using a strict, strongly-typed ruleset.
Source of truth: references/xstate-v5-rules.md (this repo, resolved from the skill root). If anything in this skill conflicts with the rules file, follow the rules file.
Inlined Non-Negotiable Contract (Agent Must Follow)
These requirements are intentionally duplicated here so an agent can comply without first loading external references:
- Design first, code second:
- Produce planning artifacts before implementation (state inventory, event catalog, transition table, async/actor map, acceptance-test scenarios).
- Include a boundary/decomposition decision record (what is split vs orchestrated, and why).
- No god-machine architecture:
- Unrelated domains (auth, toasts, navigation, workflows, transport/retry policy) must not be fully modeled in one machine.
- A single machine is acceptable only as a thin orchestration root or tightly coupled app-shell parallel regions.
- No state-mirroring context flags:
- Do not duplicate mode in context booleans (
isLoading, isAuthenticated, etc.) when state.value already represents the mode.
- If a temporary migration flag exists, document rationale and removal plan.
- XState v5 strictness:
- Use
setup({...}).createMachine({...}).
- Implement
actions/guards/actors in setup(...).
- Send event objects only.
- Avoid forbidden v4 patterns (
interpret, Machine, cond, send, pure, choose, etc.).
- Enforce explicit async and actor boundaries:
- Request/response:
invoke with onDone and onError.
- Long-lived collaborators:
spawnChild/stopChild and explicit routing via sendTo.
Fail-closed rule:
- If any detail is ambiguous, load
references/xstate-v5-rules.md before writing or reviewing machine code.
- If that file is missing/unreadable, stop and report the issue instead of guessing.
When To Use
Use this skill when the task involves:
- XState v5 machines/actors (
xstate, @xstate/react)
- Refactoring XState code to be more type-safe and idiomatic v5
- Migrating XState v4 patterns (Machine/interpret/cond/send/pure/choose/etc.) to v5 equivalents
- Planning/designing statecharts prior to implementing them as XState v5 machines
- Designing actor boundaries (invoke vs spawnChild) and React integration via
createActorContext
Hard Requirements (Enforced)
- Prefer
setup({...}).createMachine({...}) for all machines.
- Implement all
actions, guards, and actors in the setup({...}) object.
- Prefer passing event-derived data via typed
params to actions/guards.
- Never send string events. Always send event objects:
actor.send({ type: '...' }).
- Forbid XState v4 legacy APIs and patterns listed in
references/xstate-v5-rules.md.
- Do not model unrelated domains (auth, notifications/toasts, navigation, data lifecycle, etc.) inside one machine, except a thin orchestration machine.
- Do not mirror state in context booleans (
isLoading, isAuthenticated, etc.) when state.value already represents that mode.
Rules Table Of Contents
- Statechart Design: 0 Statechart Design (Before You Code)
- Core Principles: 1 Machine Creation Pattern, 2 Type Safety First
- Setup Object Rules: 3 Implementation Placement, 4 Parameter Typing
- Event Handling Rules: 5 Event Type Safety with assertEvent, 6 Event Object Requirement
- Deprecated Pattern Prevention: 7 Forbidden v4 Patterns, 8 Modern v5 Equivalents
- Context and Input Rules: 9 Context Initialization, 10 Context Updates
- Invoke and Actor Rules: 11 Invoke Configuration, 12 Actor Spawning
- Type Helper Rules: 13 Type Helpers
- Testing Rules: 14 Type-Safe Testing (14.1 Deterministic Actor Tests, 14.2 Model-Based Testing, 14.3 Graph Utilities)
- XState React Rules: 15 Shared State via createActorContext
- Best Practices Summary: 16 Code Organization, 17 Performance Considerations, 18 Error Handling, 19 Documentation
- Enforcement Rules for AI Agents: 20 Mandatory Patterns, 21 Quality Assurance, 22 When Docs Are Ambiguous
Quick Start
Minimal typed machine skeleton:
import { setup, assign } from "xstate"
type Ctx = { count: number }
type Ev = { type: "inc" } | { type: "add"; amount: number } | { type: "reset" }
type Input = { initialCount?: number }
export const counterMachine = setup({
types: {
context: {} as Ctx,
events: {} as Ev,
input: {} as Input,
},
actions: {
inc: assign({ count: ({ context }) => context.count + 1 }),
add: assign({
count: ({ context }, params: { amount: number }) => context.count + params.amount,
}),
reset: assign({ count: ({ input }) => input.initialCount ?? 0 }),
},
}).createMachine({
id: "counter",
context: ({ input }) => ({ count: input.initialCount ?? 0 }),
on: {
inc: { actions: "inc" },
add: {
actions: {
type: "add",
params: ({ event }) => ({ amount: event.amount }),
},
},
reset: { actions: "reset" },
},
})
React shared state skeleton:
import { createActorContext, shallowEqual } from "@xstate/react"
import { type SnapshotFrom } from "xstate"
import { counterMachine } from "./counterMachine"
const CounterCtx = createActorContext(counterMachine)
export const CounterProvider = CounterCtx.Provider
export const useCounterSelector = CounterCtx.useSelector
export const useCounterActorRef = CounterCtx.useActorRef
const selectCount = (s: SnapshotFrom<typeof counterMachine>) => s.context.count
export const useCount = () => useCounterSelector(selectCount)
const selectCtx = (s: SnapshotFrom<typeof counterMachine>) => s.context
export const useCounterContext = () => useCounterSelector(selectCtx, shallowEqual)
Workflow (What To Do Each Time)
- Read
references/xstate-v5-rules.md first.
- Identify the target surface area:
- Plain machine/actor (non-React)
- React integration (
createActorContext) or local useActor
- Migration from v4 patterns
- Design the statechart first:
- See
references/xstate-v5-rules.md#0-statechart-design-before-you-code.
- Write down: state inventory, event catalog (payload + source), transition table, async/actor boundaries, and acceptance-test scenarios.
- Record a boundary/decomposition decision before type design:
- Which concerns belong in separate actors/machines (based on lifecycle, owner, and failure mode).
- Which concerns are intentionally orchestrated at top-level (and why).
- Any exception note if keeping logic in one machine.
- Default: do this unless the user explicitly asks to skip planning.
- Design types first:
types.context, types.events, and types.input (and types.output if applicable)
- Use
zod schemas only if the codebase already uses Zod or the user asks for it.
- Put implementations in
setup({ actions, guards, actors }):
- Actions/guards take typed
params when they need event-derived data.
- Only read event-specific fields inside implementations when necessary; use
assertEvent then.
- Choose async boundaries:
- Prefer
invoke with typed input for request/response flows.
- Use
spawnChild/stopChild for long-lived child actors.
- Use v5 runtime APIs:
createActor(machine) (not interpret)
raise / sendTo (not send action)
enqueueActions (not pure/choose)
- Validate:
tsc passes; no any leaks in params.
- No string event sends.
- No v4 forbidden imports or config keys (
cond, withContext, withConfig, etc.).
- Prefer
waitFor for async actor tests and xstate/graph (createTestModel, getShortestPaths, etc.) for model/graph-driven test generation.
When A Monolith Is Acceptable
Use a single top-level machine only when it is a thin orchestration root that coordinates child actors or tightly coupled parallel app-shell concerns. Even then, unrelated domain logic should live in separate actors/machines and communicate via explicit events.
Review Checklist (Use When Auditing PRs)
- All machines are
setup(...).createMachine(...).
- Machine has a clear statechart plan (states/events/transition table) or the PR description includes it.
- No domain-smell: one machine has one responsibility or explicit orchestration scope.
- No state-mirroring context flags without documented rationale.
- No implementations inside
types.
actions/guards read event fields only via params or assertEvent.
- No
interpret, Machine, withConfig, withContext, cond, send, pure, choose.
invoke has onError (and onDone when appropriate).
- React usage prefers
createActorContext at module scope; selectors are stable and use shallowEqual for objects.
Common Fixes
Notes
If official docs feel ambiguous, follow the rules file guidance: confirm behavior against XState v5 source, issues/discussions, or a minimal reproduction/type-test rather than guessing.
1---2name: xstate-v53description: Implement, refactor, and review XState v5 state machines (TypeScript and React) using a strict setup().createMachine() ruleset, params-first typing, canonical actor/spawn patterns, and createActorContext() patterns from @xstate/react. Includes guidance for planning/designing statecharts before implementation.4---56# xstate-v5 (Strict) Skill78Build, refactor, and review **XState v5** state machines (TypeScript and React) using a strict, strongly-typed ruleset.910**Source of truth:** `references/xstate-v5-rules.md` (this repo, resolved from the skill root). If anything in this skill conflicts with the rules file, follow the rules file.1112## Inlined Non-Negotiable Contract (Agent Must Follow)1314These requirements are intentionally duplicated here so an agent can comply without first loading external references:1516- Design first, code second:17 - Produce planning artifacts before implementation (state inventory, event catalog, transition table, async/actor map, acceptance-test scenarios).18 - Include a boundary/decomposition decision record (what is split vs orchestrated, and why).19- No god-machine architecture:20 - Unrelated domains (auth, toasts, navigation, workflows, transport/retry policy) must not be fully modeled in one machine.21 - A single machine is acceptable only as a thin orchestration root or tightly coupled app-shell parallel regions.22- No state-mirroring context flags:23 - Do not duplicate mode in context booleans (`isLoading`, `isAuthenticated`, etc.) when `state.value` already represents the mode.24 - If a temporary migration flag exists, document rationale and removal plan.25- XState v5 strictness:26 - Use `setup({...}).createMachine({...})`.27 - Implement `actions`/`guards`/`actors` in `setup(...)`.28 - Send event objects only.29 - Avoid forbidden v4 patterns (`interpret`, `Machine`, `cond`, `send`, `pure`, `choose`, etc.).30- Enforce explicit async and actor boundaries:31 - Request/response: `invoke` with `onDone` and `onError`.32 - Long-lived collaborators: `spawnChild`/`stopChild` and explicit routing via `sendTo`.3334Fail-closed rule:35- If any detail is ambiguous, load `references/xstate-v5-rules.md` before writing or reviewing machine code.36- If that file is missing/unreadable, stop and report the issue instead of guessing.3738## When To Use3940Use this skill when the task involves:4142- XState v5 machines/actors (`xstate`, `@xstate/react`)43- Refactoring XState code to be more type-safe and idiomatic v544- Migrating XState v4 patterns (Machine/interpret/cond/send/pure/choose/etc.) to v5 equivalents45- Planning/designing statecharts prior to implementing them as XState v5 machines46- Designing actor boundaries (invoke vs spawnChild) and React integration via `createActorContext`4748## Hard Requirements (Enforced)4950- Prefer `setup({...}).createMachine({...})` for all machines.51- Implement **all** `actions`, `guards`, and `actors` in the `setup({...})` object.52- Prefer passing event-derived data via typed `params` to actions/guards.53- Never send string events. Always send event objects: `actor.send({ type: '...' })`.54- Forbid XState v4 legacy APIs and patterns listed in `references/xstate-v5-rules.md`.55- Do not model unrelated domains (auth, notifications/toasts, navigation, data lifecycle, etc.) inside one machine, except a thin orchestration machine.56- Do not mirror state in context booleans (`isLoading`, `isAuthenticated`, etc.) when `state.value` already represents that mode.5758## Rules Table Of Contents5960- Statechart Design: [0 Statechart Design (Before You Code)](references/xstate-v5-rules.md#0-statechart-design-before-you-code)61- Core Principles: [1 Machine Creation Pattern](references/xstate-v5-rules.md#1-machine-creation-pattern), [2 Type Safety First](references/xstate-v5-rules.md#2-type-safety-first)62- Setup Object Rules: [3 Implementation Placement](references/xstate-v5-rules.md#3-implementation-placement), [4 Parameter Typing](references/xstate-v5-rules.md#4-parameter-typing)63- Event Handling Rules: [5 Event Type Safety with assertEvent](references/xstate-v5-rules.md#5-event-type-safety-with-assertevent), [6 Event Object Requirement](references/xstate-v5-rules.md#6-event-object-requirement)64- Deprecated Pattern Prevention: [7 Forbidden v4 Patterns](references/xstate-v5-rules.md#7-forbidden-v4-patterns), [8 Modern v5 Equivalents](references/xstate-v5-rules.md#8-modern-v5-equivalents)65- Context and Input Rules: [9 Context Initialization](references/xstate-v5-rules.md#9-context-initialization), [10 Context Updates](references/xstate-v5-rules.md#10-context-updates)66- Invoke and Actor Rules: [11 Invoke Configuration](references/xstate-v5-rules.md#11-invoke-configuration), [12 Actor Spawning](references/xstate-v5-rules.md#12-actor-spawning)67- Type Helper Rules: [13 Type Helpers](references/xstate-v5-rules.md#13-type-helpers)68- Testing Rules: [14 Type-Safe Testing](references/xstate-v5-rules.md#14-type-safe-testing) ([14.1 Deterministic Actor Tests](references/xstate-v5-rules.md#141-deterministic-actor-tests), [14.2 Model-Based Testing](references/xstate-v5-rules.md#142-model-based-testing-with-xstategraph), [14.3 Graph Utilities](references/xstate-v5-rules.md#143-graph-utilities))69- XState React Rules: [15 Shared State via createActorContext](references/xstate-v5-rules.md#15-shared-state-via-createactorcontext)70- Best Practices Summary: [16 Code Organization](references/xstate-v5-rules.md#16-code-organization), [17 Performance Considerations](references/xstate-v5-rules.md#17-performance-considerations), [18 Error Handling](references/xstate-v5-rules.md#18-error-handling), [19 Documentation](references/xstate-v5-rules.md#19-documentation)71- Enforcement Rules for AI Agents: [20 Mandatory Patterns](references/xstate-v5-rules.md#20-mandatory-patterns), [21 Quality Assurance](references/xstate-v5-rules.md#21-quality-assurance), [22 When Docs Are Ambiguous](references/xstate-v5-rules.md#22-when-docs-are-ambiguous)7273## Quick Start7475Minimal typed machine skeleton:7677```ts78import { setup, assign } from "xstate"7980type Ctx = { count: number }8182type Ev = { type: "inc" } | { type: "add"; amount: number } | { type: "reset" }8384type Input = { initialCount?: number }8586export const counterMachine = setup({87 types: {88 context: {} as Ctx,89 events: {} as Ev,90 input: {} as Input,91 },92 actions: {93 inc: assign({ count: ({ context }) => context.count + 1 }),94 add: assign({95 count: ({ context }, params: { amount: number }) => context.count + params.amount,96 }),97 reset: assign({ count: ({ input }) => input.initialCount ?? 0 }),98 },99}).createMachine({100 id: "counter",101 context: ({ input }) => ({ count: input.initialCount ?? 0 }),102 on: {103 inc: { actions: "inc" },104 add: {105 actions: {106 type: "add",107 params: ({ event }) => ({ amount: event.amount }),108 },109 },110 reset: { actions: "reset" },111 },112})113```114115React shared state skeleton:116117```ts118import { createActorContext, shallowEqual } from "@xstate/react"119import { type SnapshotFrom } from "xstate"120import { counterMachine } from "./counterMachine"121122const CounterCtx = createActorContext(counterMachine)123124export const CounterProvider = CounterCtx.Provider125export const useCounterSelector = CounterCtx.useSelector126export const useCounterActorRef = CounterCtx.useActorRef127128const selectCount = (s: SnapshotFrom<typeof counterMachine>) => s.context.count129export const useCount = () => useCounterSelector(selectCount)130131const selectCtx = (s: SnapshotFrom<typeof counterMachine>) => s.context132export const useCounterContext = () => useCounterSelector(selectCtx, shallowEqual)133```134135## Workflow (What To Do Each Time)1361371. Read `references/xstate-v5-rules.md` first.1382. Identify the target surface area:139 - Plain machine/actor (non-React)140 - React integration (`createActorContext`) or local `useActor`141 - Migration from v4 patterns1423. Design the statechart first:143 - See `references/xstate-v5-rules.md#0-statechart-design-before-you-code`.144 - Write down: state inventory, event catalog (payload + source), transition table, async/actor boundaries, and acceptance-test scenarios.145 - Record a boundary/decomposition decision before type design:146 - Which concerns belong in separate actors/machines (based on lifecycle, owner, and failure mode).147 - Which concerns are intentionally orchestrated at top-level (and why).148 - Any exception note if keeping logic in one machine.149 - Default: do this unless the user explicitly asks to skip planning.1504. Design types first:151 - `types.context`, `types.events`, and `types.input` (and `types.output` if applicable)152 - Use `zod` schemas only if the codebase already uses Zod or the user asks for it.1535. Put implementations in `setup({ actions, guards, actors })`:154 - Actions/guards take typed `params` when they need event-derived data.155 - Only read event-specific fields inside implementations when necessary; use `assertEvent` then.1566. Choose async boundaries:157 - Prefer `invoke` with typed `input` for request/response flows.158 - Use `spawnChild`/`stopChild` for long-lived child actors.1597. Use v5 runtime APIs:160 - `createActor(machine)` (not `interpret`)161 - `raise` / `sendTo` (not `send` action)162 - `enqueueActions` (not `pure`/`choose`)1638. Validate:164 - `tsc` passes; no `any` leaks in params.165 - No string event sends.166 - No v4 forbidden imports or config keys (`cond`, `withContext`, `withConfig`, etc.).167 - Prefer `waitFor` for async actor tests and `xstate/graph` (`createTestModel`, `getShortestPaths`, etc.) for model/graph-driven test generation.168169## When A Monolith Is Acceptable170171Use a single top-level machine only when it is a thin orchestration root that coordinates child actors or tightly coupled parallel app-shell concerns. Even then, unrelated domain logic should live in separate actors/machines and communicate via explicit events.172173## Review Checklist (Use When Auditing PRs)174175- All machines are `setup(...).createMachine(...)`.176- Machine has a clear statechart plan (states/events/transition table) or the PR description includes it.177- No domain-smell: one machine has one responsibility or explicit orchestration scope.178- No state-mirroring context flags without documented rationale.179- No implementations inside `types`.180- `actions`/`guards` read event fields only via `params` or `assertEvent`.181- No `interpret`, `Machine`, `withConfig`, `withContext`, `cond`, `send`, `pure`, `choose`.182- `invoke` has `onError` (and `onDone` when appropriate).183- React usage prefers `createActorContext` at module scope; selectors are stable and use `shallowEqual` for objects.184185## Common Fixes186187- Event payload used inside an action:188 - Prefer: compute payload in `params` and make the action implementation depend on `params`.189 - Otherwise: add `assertEvent(event, 'someType')` inside the implementation.190191- v4 `cond`:192 - Rename to `guard` and move guard implementation into `setup({ guards: { ... } })`.193194- v4 `interpret`:195 - Replace with `createActor(machine)` and `.start()` where needed.196197## Notes198199If official docs feel ambiguous, follow the rules file guidance: confirm behavior against XState v5 source, issues/discussions, or a minimal reproduction/type-test rather than guessing.