About xstate-v5
xstate-v5 is a strict Codex skill for designing, implementing, refactoring, and reviewing typed XState v5 state machines in TypeScript and React. It requires statechart planning before code and sets rules for actor boundaries, event typing, and avoiding legacy v4 patterns.
Key features
- 01Statechart-first design artifacts
- 02Strict XState v5 API conventions
- 03Typed events, context, and input
- 04Explicit invoke and child actor boundaries
- 05React actor-context guidance
Use cases
- 01Planning a new XState v5 state machine
- 02Refactoring or reviewing XState v5 code
- 03Migrating legacy XState v4 patterns
- 04Designing actor boundaries in React applications
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ johnlombardo-dev/ xstate-skill --skill xstate-v5After installing, type/xstate-v5in Claude Code to confirm it loaded.
Details
From the project READMExstate-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.) whenstate.valuealready represents the mode. - If a temporary migration flag exists, document rationale and removal plan.
- Do not duplicate mode in context booleans (
- XState v5 strictness:
- Use
setup({...}).createMachine({...}). - Implement
actions/guards/actorsinsetup(...). - Send event objects only.
- Avoid forbidden v4 patterns (
interpret,Machine,cond,send,pure,choose, etc.).
- Use
- Enforce explicit async and actor boundaries:
- Request/response:
invokewithonDoneandonError. - Long-lived collaborators:
spawnChild/stopChildand explicit routing viasendTo.
- Request/response:
Fail-closed rule:
- If any detail is ambiguous, load
references/xstate-v5-rules.mdbefore 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, andactorsin thesetup({...})object. - Prefer passing event-derived data via typed
paramsto 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.) whenstate.valuealready 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.mdfirst. - Identify the target surface area:
- Plain machine/actor (non-React)
- React integration (
createActorContext) or localuseActor - 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.
- See
- Design types first:
types.context,types.events, andtypes.input(andtypes.outputif applicable)- Use
zodschemas only if the codebase already uses Zod or the user asks for it.
- Put implementations in
setup({ actions, guards, actors }):- Actions/guards take typed
paramswhen they need event-derived data. - Only read event-specific fields inside implementations when necessary; use
assertEventthen.
- Actions/guards take typed
- Choose async boundaries:
- Prefer
invokewith typedinputfor request/response flows. - Use
spawnChild/stopChildfor long-lived child actors.
- Prefer
- Use v5 runtime APIs:
createActor(machine)(notinterpret)raise/sendTo(notsendaction)enqueueActions(notpure/choose)
- Validate:
tscpasses; noanyleaks in params.- No string event sends.
- No v4 forbidden imports or config keys (
cond,withContext,withConfig, etc.). - Prefer
waitForfor async actor tests andxstate/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/guardsread event fields only viaparamsorassertEvent.- No
interpret,Machine,withConfig,withContext,cond,send,pure,choose. invokehasonError(andonDonewhen appropriate).- React usage prefers
createActorContextat module scope; selectors are stable and useshallowEqualfor objects.
Common Fixes
-
Event payload used inside an action:
- Prefer: compute payload in
paramsand make the action implementation depend onparams. - Otherwise: add
assertEvent(event, 'someType')inside the implementation.
- Prefer: compute payload in
-
v4
cond:- Rename to
guardand move guard implementation intosetup({ guards: { ... } }).
- Rename to
-
v4
interpret:- Replace with
createActor(machine)and.start()where needed.
- Replace with
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.