
Xstate
- 532 installs
- 56 repo stars
- Updated August 4, 2026
- seed-hypermedia/seed
xstate is an agent-building skill that models complex state-driven behaviors with visual finite-state machines interpretable by Claude, Cursor, and other coding agents for developers orchestrating multi-step agent logic.
About
xstate is a seed-hypermedia/seed skill helping developers define agent behaviors as explicit finite-state machines instead of ad-hoc prompt chains. State charts make transitions, guards, and side effects visible so Claude, Cursor, and compatible agents can read, debug, and execute predictable workflows. With 506 installs on skills.sh, it targets teams building agents with branching tool calls, retries, and human-in-the-loop steps. Reach for xstate when agent logic grows beyond linear scripts, when you need auditable state diagrams, or when multiple agents must share the same behavioral spec.
- Defines finite state machines that prevent invalid agent transitions
- Visual editor compatible with XState visualizer tools
- Generates executable TypeScript guards, actions, and services
- Enables predictable agent orchestration across multi-step workflows
- Produces machine definitions that serve as reliable handoff artifacts to nextSkills
Xstate by the numbers
- 532 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,701 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/seed-hypermedia/seed --skill xstateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 532 |
|---|---|
| repo stars | ★ 56 |
| Last updated | August 4, 2026 |
| Repository | seed-hypermedia/seed ↗ |
How do you model agent behavior with state machines?
Model complex, state-driven agent behaviors with visual finite-state machines that Claude, Cursor, and other agents can interpret and execute.
Who is it for?
Developers building multi-step agents who need explicit state charts instead of unstructured prompt chains.
Skip if: Developers with simple linear scripts that do not need guards, transitions, or visual state modeling.
When should I use this skill?
User needs finite-state machines, XState charts, or visual agent behavior models with branching and guards.
What you get
Visual finite-state machine definitions and interpretable agent state charts
- State machine definitions
- Visual state charts
- Agent-interpretable transition specs
By the numbers
- 506 installs on skills.sh
- Rank 9445 on skills.sh catalog
Files
XState v5
Use this skill for state machine and statechart engineering first and API correctness second.
This skill is v5-only. When examples, blog posts, answers, or local code smell v4-ish, translate them rather than mixing versions. Prefer local repo code and official v5 docs over generic memory.
Your job:
- choose between
xstateand@xstate/store - design a sound machine or actor system from messy requirements
- write modern XState v5 TypeScript code in a consistent style
- review, repair, or improve existing XState code
- migrate legacy v4-ish patterns when they appear
- choose the right actor kind and action shape when the problem is not just
assign(...)plusfromPromise(...) - connect machines and actors to
@xstate/react,@xstate/vue,@xstate/svelte, or@xstate/solidwhen needed
First pass
In an existing codebase:
1. Inspect local package.json files and imports. 2. Read nearby machines, stores, actors, and adapter usage. 3. Distinguish between new code, local edits, and migration work before choosing how opinionated to be. 4. Preserve surrounding style by default.
Task mode
Choose a mode before writing code:
- New code: prefer the modern v5 patterns in this skill.
- Local edit: preserve local structure and naming unless the current code is mixed, broken, or cleanup was
requested.
- Migration: prefer the smallest safe translation to v5. Preserve structure and semantics unless broader
normalization was requested.
For migration and local edits, do not introduce setup(...), named implementations, object-form guards/actions, tags, or actor decomposition unless they are required for correctness, significantly reduce local complexity, or were explicitly requested.
Choose the tool
Prefer @xstate/store when the domain is simple event-based state management:
- no meaningful finite modes that need coordination, orchestration, or explicit lifecycle modeling
- no invoked async processes
- no actor communication
- no need for state machine or statechart concepts such as guarded transitions, delayed transitions, parallel states, or
history
Simple fetching or mutation logic can still fit @xstate/store if it does not need machine-level orchestration.
Prefer XState when the domain has one or more of these:
- finite modes that matter to behavior or UI
- async workflows, retries, cancellation, or background processes
- multiple interacting processes or child actors
- explicit guards, delays, tags, or transition rules
- a need to model business process flow, not just update state
If the problem is too simple for a machine, say so plainly and recommend @xstate/store.
Workflow
When requirements are fuzzy:
1. Sketch the machine shape first. 2. Name the important states, events, context, actors, and tags. 3. Call out uncertainty or tradeoffs briefly. 4. Then write the code.
When requirements are already clear, keep the sketch short or implicit and move to code.
Do not over-refactor existing code for conceptual purity. Improve the model where it matters, but preserve working structure when a larger rewrite is not justified.
Modeling questions
Use these questions before writing or revising code:
- What are the finite states? Put modes in states, not in context.
- What belongs in context? Keep only durable data, not derivable booleans or duplicated mode.
- What are the domain events? Prefer meaningful names, often dot-separated, such as
form.submittedor
order.confirmed.
- What should be a guard versus a separate branch state?
- What side effects should be invoked actors versus transition actions?
- Can an event carry the needed data instead of stashing temporary relay data in context for a later state?
- Is
fromPromise(...)actually the right actor, or is this a callback/subscription protocol that should send events
back over time?
- Should the machine emit events to the surrounding system instead of forcing consumers to infer everything from
context?
- Should a child concern be a spawned/invoked actor instead of more parent complexity?
- Does the parent really need these extra intermediate states, or should a child actor own that internal detail?
- Which states need tags for UI semantics such as
'loading','error', or'dirty'? - Can the UI use
snapshot.matches(...), tags, orsnapshot.can(...)instead of extra booleans?
Preferred v5 patterns
For new code, prefer these patterns unless the local codebase has a strong reason not to:
- TypeScript only.
- Prefer
setup({...}).createMachine({...}). - Define named
actions,guards,actors, anddelaysinsetup(...). - Prefer transition objects like
{ target: 'next' }over shorthand when it improves consistency. - Prefer arrays for
actions, even for a single action, when that keeps the shape consistent. - Prefer action and guard objects such as
{ type: 'track' }or{ type: 'isValid', params: ... }when the intent is
reusable or named.
- Prefer
tags: []for UI semantics and cross-cutting state meaning. - Prefer domain-oriented event names; preserve local naming conventions if they are already established.
- Prefer plain functions for derived values instead of storing derivable data in context.
- Prefer event payloads over temporary context relay when one state only needs to pass data to the next step.
- Prefer passing data into named actions and guards via
paramsinstead of reaching intoeventdirectly. - Do not treat
assign(...)as the only action pattern. For typed reusable logic beyond simple context updates,
consider setup-scoped helpers from a const machineSetup = setup(...) object, such as machineSetup.createAction(...), machineSetup.enqueueActions(...), and machineSetup.emit(...), or named action objects when they fit the behavior better.
- When a named
assign(...)action updates multiple context properties, every property updater shares the sameparams
type. Use one coherent params object for all updated fields, or split the work into separate named actions. Do not mix incompatible params shapes inside one assigner.
- When an implementation must inspect
eventand the type is not obvious, preferassertEvent(...)for narrowing. - Avoid
as anyand other loose casts in examples and final code unless there is no cleaner local option. - Choose actor logic intentionally: prefer
fromPromise(...)for one request/one result, and preferfromCallback(...)
for subscriptions, timers, external callbacks, and multi-event protocols that send events back over time.
- Consider
emit(...)when the machine should notify the surrounding system directly. - Keep parent states coarse when possible. If extra intermediate states mainly model an implementation detail, prefer
letting a child actor own that internal behavior.
- If you show multiple files, wire them completely. Include real imports/exports for referenced modules and components,
or keep the example smaller.
Keep event naming consistent. The . in event names is useful and meaningful, including for partial event descriptors and clearer domain grouping. See the official docs on events and transitions and TypeScript narrowing with `assertEvent(...)`.
See references/examples.md for canonical code shapes, references/advanced-patterns.md when the task involves typed actions beyond assign(...), callback actors, emitted events, or persistence/hydration, and references/observables-and-inspection.md for fromObservable(...), inspection, and browser inspector patterns.
UI integration
Prefer letting the machine drive UI behavior:
- use
snapshot.matches(...)for finite mode checks - use tags for semantic checks such as loading, saving, or dirty
- use
snapshot.can(...)to drive whether an event is currently valid - avoid parallel piles of manual booleans that restate machine truth
If a component owns a small local actor, useMachine(...) is often enough.
If an actor is shared, long-lived, or performance-sensitive:
- create or obtain an actor ref once
- read slices with
useSelector(...) - use actor context/provider helpers when the actor is shared across a subtree
See references/adapters.md for concise adapter guidance, and references/react.md for React hook selection, input wiring, nested-state matching, and custom hook patterns.
Review and repair guidance
When reviewing or fixing XState code, look for:
- finite mode stored in context instead of states
- giant machines that should be split into child actors
- side effects hidden in random callbacks or mixed into assigners
- v4 and v5 concepts mixed together in the same machine
- state names and event names that describe UI mechanics instead of domain meaning
- extra booleans that should be replaced by
matches(...), tags, or selectors - missed opportunities to use
@xstate/storewhen the problem is simple - named
assign(...)actions whose property updaters implicitly expect differentparamsshapes
Avoid pushing decomposition too far. Actor boundaries are useful when they improve clarity, ownership, or concurrency. Do not split for its own sake.
Migration
When legacy code is present, translate toward v5 gradually and locally unless the user asked for a broader migration.
Common migration targets include:
cond->guardschema->typesservices->actorsinterpret(...)->createActor(...)- old function signatures -> destructured v5 arguments such as
({ context, event })
For migration tasks:
- preserve local structure first
- change only what is needed for correctness, compatibility, or clarity
- do not normalize into the full preferred house style unless the user asked for that
- explain any non-local refactor you choose to make
- do not preserve dead or invalid hook option patterns just because they existed nearby
- when migrating React usage, prefer valid current hook surfaces over trying to smuggle old
interpret-style
implementation overrides into useMachine(...)
Use references/v4-to-v5-quick-ref.md for quick translation patterns.
Persistence
When persistence or hydration matters:
- persist snapshots, not just context, unless context-only restore is truly sufficient
- prefer
actor.getPersistedSnapshot()for saving machine state - prefer hydrating with
createActor(machine, { snapshot })or adapter/provider snapshot options where available - do not hand-roll restoration by guessing the state value plus a partial context blob
See references/advanced-patterns.md for concrete persistence and hydration patterns.
Testing
Do not introduce testing guidance unless the user asks about testing or requests tests.
If they do ask, keep it brief:
- test key transitions and guards
- test happy path and failure path actor behavior
- test the UI against machine state rather than duplicating stateful logic
- mention model-based testing utilities when useful
If a link would help, point them to the XState testing and graph/path generation docs.
Optional tools
Stately Studio and inspection tools can help with design, debugging, and communication, but they are optional. Mention them when the user is designing a machine, wants visualization, or needs better debugging visibility.
- Stately Studio for visual modeling and collaboration
- Inspection API for observing actor systems
- Stately Inspector for visual inspection in running apps
Output format
Prefer this output shape:
1. Short machine sketch if requirements are fuzzy. 2. Code. 3. Brief rationale explaining states, events, context, actors, and tags. 4. Migration notes only if relevant.
Prefer fewer complete files over a larger but partially wired example. Do not sketch extra shell files unless they are fully wired.
See references/examples.md for more canonical examples that can grow over time.
Final self-check
Before you answer, do a quick compile-minded pass:
- every JSX component from another file has a real import
- every imported symbol is actually exported by the shown file
- every
send(...)andsnapshot.can(...)call uses real event objects - async work uses
actors/fromPromiseandinvoke.input, not legacy shapes - if you reference an external helper from the prompt or surrounding system, declare its signature or show enough typed
shape for the example to stand on its own
- migration examples do not pass invalid implementation override objects into hooks just to preserve dead local code
- no placeholder shell files, omitted imports,
..., or pseudocode in code fences that are meant to be runnable
If a complete multi-file answer is getting bulky, remove the shell file and keep the smaller set of files that still demonstrates the pattern correctly.
{
"skill_name": "xstate-v5",
"evals": [
{
"id": 1,
"prompt": "migrate this to xstate v5\n\n```ts\nimport { createMachine, assign } from 'xstate';\nimport { useInterpret, useActor } from '@xstate/react';\n\nconst machine = createMachine({\n schema: {\n context: {} as { count: number },\n events: {} as { type: 'inc' } | { type: 'reset' }\n },\n context: { count: 0 },\n on: {\n inc: {\n cond: (context) => context.count < 10,\n actions: assign({ count: (context) => context.count + 1 })\n },\n reset: {\n actions: assign({ count: 0 })\n }\n }\n});\n\nexport function Counter() {\n const service = useInterpret(machine, {\n actions: {\n logReset: () => console.log('reset')\n }\n });\n const [state, send] = useActor(service);\n\n return <button onClick={() => send({ type: 'inc' })}>{state.context.count}</button>;\n}\n```",
"expected_output": "A minimal local v5 migration that fixes deprecated APIs without turning the example into a broader redesign.",
"files": [],
"expectations": [
"The migrated code replaces `schema` with `types`.",
"The migrated code replaces `cond` with `guard`.",
"The answer replaces deprecated React hook usage with a valid v5-safe pattern, such as `useMachine(...)` for a locally owned actor or `useActorRef()` plus `useSelector(...)` for an existing/shared actor.",
"The answer does not introduce a broad redesign such as splitting the component into new providers or contexts when a local migration is sufficient.",
"The answer briefly explains the migration choices.",
"The extracted TypeScript compiles under the eval typecheck harness."
]
},
{
"id": 2,
"prompt": "what's the xstate/react pattern for a shared auth actor? navbar, settings page, and billing page all need it and i don't want every component creating its own machine or rerendering all the time. show me code. login and session refresh are async.",
"expected_output": "A shared-actor React pattern with modern v5 async actor wiring and complete code that typechecks.",
"files": [],
"expectations": [
"The answer recommends a shared actor pattern such as `createActorContext(...)` or a shared actor ref plus provider/context.",
"The answer does not recommend calling `useMachine(...)` separately in each consuming component as the primary pattern.",
"The answer uses or mentions `useSelector(...)` for finer-grained subscriptions or rerender control.",
"The answer uses valid v5 async actor wiring such as `actors`/`fromPromise` and `invoke.input`, rather than legacy `services` or `invoke.data` patterns.",
"If the answer splits the example across files, its imports, exports, and component references are wired coherently.",
"The extracted TypeScript compiles under the eval typecheck harness.",
"The answer briefly explains why the chosen pattern fits shared/global actor usage."
]
},
{
"id": 3,
"prompt": "fix this xstate machine\n\n```ts\nimport { createMachine, assign, fromPromise } from 'xstate';\n\nconst uploadMachine = createMachine({\n context: {\n uploadId: null as string | null,\n progress: 0,\n latestChunk: null as { loaded: number; total: number } | null,\n error: null as string | null\n },\n initial: 'idle',\n states: {\n idle: {\n on: {\n 'upload.started': {\n target: 'uploading',\n actions: assign({\n uploadId: ({ event }) => event.uploadId\n })\n }\n }\n },\n uploading: {\n invoke: {\n src: fromPromise(async ({ input }) => {\n return startUploadAndListen(input.uploadId);\n }),\n input: ({ context }) => ({ uploadId: context.uploadId }),\n onDone: [\n {\n guard: ({ event }) => event.output.type === 'progress',\n target: 'uploading',\n actions: assign({\n latestChunk: ({ event }) => event.output.chunk,\n progress: ({ event }) => Math.round((event.output.chunk.loaded / event.output.chunk.total) * 100)\n })\n },\n {\n target: 'done'\n }\n ],\n onError: {\n target: 'failed',\n actions: assign({\n error: ({ event }) => String(event.error)\n })\n }\n }\n },\n done: {},\n failed: {}\n }\n});\n```",
"expected_output": "A local repair that replaces the awkward promise-based stream handling with a better evented actor shape and avoids unnecessary context relay.",
"files": [],
"expectations": [
"The answer replaces the awkward `fromPromise(...)` progress-stream pattern with a callback/subscription actor such as `fromCallback(...)`, or explicitly explains why a callback actor fits better here.",
"The answer models repeated progress updates as events sent back over time rather than trying to branch on multiple pseudo-results in `onDone`.",
"The answer avoids storing transient relay data like `latestChunk` in context when a simpler event-driven shape is sufficient.",
"The answer keeps the parent machine reasonably coarse instead of adding extra parent states that mainly model the upload protocol internals.",
"The extracted TypeScript compiles under the eval typecheck harness.",
"The answer briefly explains what was wrong."
]
},
{
"id": 4,
"prompt": "show me the xstate v5 way to handle this: when a player drops a tile, if the move is valid i need to swap the tiles, clear the current selection, and notify the surrounding app with a `move.completed` event. if the move is invalid i want to keep the selection. typescript please.",
"expected_output": "A v5 solution that uses typed action composition for the valid-move branch and emits an outward-facing event without forcing everything into context or one giant custom action.",
"files": [],
"expectations": [
"The answer uses a typed action composition pattern such as setup-scoped helpers from `const machineSetup = setup(...)`, `enqueueActions(...)`, or another equally valid v5-safe batching pattern for the valid-move branch.",
"The answer emits or clearly models an outward-facing `move.completed` notification instead of only mutating context and expecting consumers to infer the move indirectly.",
"The answer does not misuse `assign(...)` imperatively inside a custom action function.",
"The answer keeps the move-validation flow local instead of exploding the machine into several extra parent states for this one branch.",
"The extracted TypeScript compiles under the eval typecheck harness.",
"The answer briefly explains why the chosen action/emission pattern fits."
]
},
{
"id": 5,
"prompt": "how do i persist and restore an xstate v5 machine across reloads in react? show me the real pattern, not pseudo-code.",
"expected_output": "A concrete persistence and hydration pattern using persisted snapshots rather than ad hoc reconstruction.",
"files": [],
"expectations": [
"The answer uses `actor.getPersistedSnapshot()` or an equivalent persisted snapshot API for saving machine state.",
"The answer restores the actor from a persisted snapshot using `snapshot`, not by manually reconstructing state from guessed `value` and partial `context`.",
"The answer shows a React-compatible usage pattern, such as actor/provider setup or hook options that hydrate from a snapshot.",
"The answer does not present context-only persistence as the default when full machine persistence is the safer pattern.",
"The extracted TypeScript compiles under the eval typecheck harness.",
"The answer briefly explains why snapshot-based persistence is preferred."
]
}
]
}
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
FENCE_RE = re.compile(r"```(?P<lang>[^\n`]*)\n(?P<code>.*?)```", re.DOTALL)
FILE_HINT_RE = re.compile(r"^\s*//\s*([A-Za-z0-9_.-]+\.(?:ts|tsx))\s*$")
HEADING_FILE_RE = re.compile(r"^\s*#+\s+`?([A-Za-z0-9_.-]+\.(?:ts|tsx))`?\s*$", re.MULTILINE)
DIAGNOSTIC_RE = re.compile(r"^(?P<file>.+?)\((?P<line>\d+),(?P<column>\d+)\): error TS(?P<code>\d+): (?P<message>.+)$")
def skill_dir_from_script(script_path: Path) -> Path:
return script_path.resolve().parents[1]
def workspace_dir(skill_dir: Path) -> Path:
return skill_dir.parent / f"{skill_dir.name}-workspace"
def harness_dir(skill_dir: Path) -> Path:
return workspace_dir(skill_dir) / "ts-harness"
def read_local_versions() -> tuple[str, str]:
xstate_pkg = Path("/Users/davidkpiano/Code/xstate/packages/core/package.json")
react_pkg = Path("/Users/davidkpiano/Code/xstate/packages/xstate-react/package.json")
if xstate_pkg.exists() and react_pkg.exists():
return (
json.loads(xstate_pkg.read_text())["version"],
json.loads(react_pkg.read_text())["version"],
)
return ("5.30.0", "6.1.0")
def ensure_harness(base_dir: Path) -> None:
base_dir.mkdir(parents=True, exist_ok=True)
xstate_version, react_version = read_local_versions()
package_json = {
"name": "xstate-v5-eval-harness",
"private": True,
"type": "module",
"dependencies": {
"xstate": xstate_version,
"@xstate/react": react_version,
"react": "^19.0.0",
"@types/react": "^19.0.0",
"typescript": "^5.9.0",
},
}
package_path = base_dir / "package.json"
marker_path = base_dir / ".versions.json"
desired_marker = {
"xstate": xstate_version,
"@xstate/react": react_version,
"react": "^19.0.0",
"@types/react": "^19.0.0",
"typescript": "^5.9.0",
}
should_install = (
not (base_dir / "node_modules").exists()
or not package_path.exists()
or not marker_path.exists()
)
if not should_install:
try:
should_install = json.loads(marker_path.read_text()) != desired_marker
except json.JSONDecodeError:
should_install = True
if should_install:
package_path.write_text(json.dumps(package_json, indent=2) + "\n")
marker_path.write_text(json.dumps(desired_marker, indent=2) + "\n")
subprocess.run(
["npm", "install", "--silent"],
cwd=base_dir,
check=True,
)
def extract_blocks(markdown: str) -> list[tuple[str, str, str | None]]:
blocks: list[tuple[str, str, str | None]] = []
for match in FENCE_RE.finditer(markdown):
lang = match.group("lang").strip().lower()
code = match.group("code").strip()
if not code:
continue
if not lang:
continue
if not lang.startswith(("ts", "js", "jsx")):
continue
heading_hint = None
for heading_match in HEADING_FILE_RE.finditer(markdown[: match.start()]):
heading_hint = heading_match.group(1)
blocks.append((lang, code, heading_hint))
return blocks
def guess_filename(code: str, lang: str, index: int, heading_hint: str | None) -> str:
first_line = code.splitlines()[0] if code.splitlines() else ""
hint_match = FILE_HINT_RE.match(first_line)
if hint_match:
return hint_match.group(1)
if heading_hint:
return heading_hint
ext = "tsx" if lang in {"tsx", "jsx"} or re.search(r"return\s*<|<\w", code) else "ts"
return f"snippet-{index}.{ext}"
def unique_name(name: str, used: set[str]) -> str:
if name not in used:
used.add(name)
return name
stem = Path(name).stem
suffix = Path(name).suffix
counter = 2
while True:
candidate = f"{stem}-{counter}{suffix}"
if candidate not in used:
used.add(candidate)
return candidate
counter += 1
def make_case_dir(base_dir: Path, requested_dir: Path | None) -> Path:
if requested_dir is not None:
requested_dir.mkdir(parents=True, exist_ok=True)
return requested_dir
cases_dir = base_dir / "cases"
cases_dir.mkdir(parents=True, exist_ok=True)
case_name = f"case-{int(time.time() * 1000)}"
case_dir = cases_dir / case_name
case_dir.mkdir()
return case_dir
def write_case_files(case_dir: Path, blocks: list[tuple[str, str, str | None]]) -> list[str]:
used: set[str] = set()
files: list[str] = []
for index, (lang, code, heading_hint) in enumerate(blocks, start=1):
filename = unique_name(guess_filename(code, lang, index, heading_hint), used)
(case_dir / filename).write_text(code + "\n")
files.append(filename)
return files
def write_tsconfig(case_dir: Path) -> None:
tsconfig = {
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": True,
"noEmit": True,
"skipLibCheck": True,
"lib": ["ES2022", "DOM"],
"types": ["react"],
},
"include": ["**/*.ts", "**/*.tsx"],
}
(case_dir / "tsconfig.json").write_text(json.dumps(tsconfig, indent=2) + "\n")
def typecheck(case_dir: Path, base_dir: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["npm", "exec", "--", "tsc", "-p", str(case_dir / "tsconfig.json"), "--pretty", "false"],
cwd=base_dir,
capture_output=True,
text=True,
)
def parse_diagnostics(output: str) -> dict[str, object]:
diagnostics: list[dict[str, object]] = []
categories = {
"missing_module_errors": 0,
"missing_name_errors": 0,
"implicit_any_errors": 0,
"assignability_errors": 0,
"syntax_errors": 0,
"other_errors": 0,
}
for line in output.splitlines():
match = DIAGNOSTIC_RE.match(line.strip())
if not match:
continue
code = int(match.group("code"))
message = match.group("message")
diagnostic = {
"file": match.group("file"),
"line": int(match.group("line")),
"column": int(match.group("column")),
"code": code,
"message": message,
}
diagnostics.append(diagnostic)
if code == 2307:
categories["missing_module_errors"] += 1
elif code == 2304:
categories["missing_name_errors"] += 1
elif code == 7006:
categories["implicit_any_errors"] += 1
elif code in {2322, 2345, 2741, 2326}:
categories["assignability_errors"] += 1
elif 1000 <= code < 2000:
categories["syntax_errors"] += 1
else:
categories["other_errors"] += 1
return {
"diagnostics": diagnostics,
"categories": categories,
"has_cross_file_reference_errors": (
categories["missing_module_errors"] > 0 or categories["missing_name_errors"] > 0
),
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("markdown_file", type=Path)
parser.add_argument("--case-dir", type=Path)
parser.add_argument("--keep-case-dir", action="store_true")
args = parser.parse_args()
script_path = Path(__file__)
skill_dir = skill_dir_from_script(script_path)
base_dir = harness_dir(skill_dir)
ensure_harness(base_dir)
markdown = args.markdown_file.read_text()
blocks = extract_blocks(markdown)
case_dir = make_case_dir(base_dir, args.case_dir)
result: dict[str, object] = {
"markdown_file": str(args.markdown_file),
"case_dir": str(case_dir),
"compile_success": False,
"files": [],
"stdout": "",
"stderr": "",
"returncode": None,
"reason": "",
}
if not blocks:
result["reason"] = "No TypeScript/TSX/JSX code fences found."
print(json.dumps(result, indent=2))
return 0
files = write_case_files(case_dir, blocks)
write_tsconfig(case_dir)
proc = typecheck(case_dir, base_dir)
result["files"] = files
result["stdout"] = proc.stdout
result["stderr"] = proc.stderr
result["returncode"] = proc.returncode
result["compile_success"] = proc.returncode == 0
result["reason"] = "ok" if proc.returncode == 0 else "TypeScript compilation failed."
result["diagnostics_summary"] = parse_diagnostics(proc.stdout)
print(json.dumps(result, indent=2))
if not args.keep_case_dir and args.case_dir is None and proc.returncode == 0:
shutil.rmtree(case_dir, ignore_errors=True)
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
def expectation_weight(text: str) -> float:
if "compiles under the eval typecheck harness" in text:
return 2.0
if "imports, exports, and component references are wired coherently" in text:
return 1.5
return 1.0
def expectation_credit(expectation: dict, grading: dict) -> float:
text = expectation["text"]
if expectation.get("passed"):
return 1.0
if "imports, exports, and component references are wired coherently" in text:
categories = (
grading.get("typecheck", {})
.get("diagnostics_summary", {})
.get("categories", {})
)
cross_file_errors = categories.get("missing_module_errors", 0) + categories.get(
"missing_name_errors", 0
)
# 1 error still gets most credit; 5+ gets none.
return max(0.0, 1.0 - (cross_file_errors / 5.0))
return 0.0
def regrade_file(path: Path) -> None:
grading = json.loads(path.read_text())
expectations = grading.get("expectations", [])
if not expectations:
return
weighted_points = 0.0
weighted_total = 0.0
breakdown = []
for expectation in expectations:
weight = expectation_weight(expectation["text"])
credit = expectation_credit(expectation, grading)
weighted_points += weight * credit
weighted_total += weight
breakdown.append(
{
"text": expectation["text"],
"weight": weight,
"credit": round(credit, 4),
"points": round(weight * credit, 4),
}
)
raw_passed = sum(1 for expectation in expectations if expectation.get("passed"))
raw_total = len(expectations)
grading["summary"]["raw_pass_rate"] = round(raw_passed / raw_total, 4) if raw_total else 0.0
grading["summary"]["pass_rate"] = round(weighted_points / weighted_total, 4) if weighted_total else 0.0
grading["summary"]["weighted_points"] = round(weighted_points, 4)
grading["summary"]["weighted_total"] = round(weighted_total, 4)
grading["weighted_breakdown"] = breakdown
path.write_text(json.dumps(grading, indent=2) + "\n")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("iteration_dir", type=Path)
args = parser.parse_args()
for grading_path in sorted(args.iteration_dir.glob("eval-*/**/grading.json")):
regrade_file(grading_path)
print(f"weighted regrade complete: {args.iteration_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Adapter Notes
Use this file for brief framework-specific guidance after the machine design is already clear.
The concepts are the same across adapters:
- create or obtain an actor ref
- read slices of snapshot state with selectors when possible
- send events to the actor
- avoid duplicating machine truth in component-local booleans
React
- Use
useMachine(machine)when the component owns a local actor. - Prefer
createActorContext(...),useActorRef(...), anduseSelector(...)when the actor is shared or when you want finer-grained subscriptions. - Prefer UI checks like
snapshot.matches(...),snapshot.hasTag(...), andsnapshot.can(...). - See
react.mdfor a deeper hook-selection table,inputwiring, nested-state matching, and the custom hook pattern.
Vue
- Use
useMachine(machine)for simple component-local ownership. - Prefer
useActorRef(...)anduseSelector(...)when working with shared actors or more selective subscriptions. - Keep template conditionals driven by snapshot state or tags.
Svelte
- Use
useMachine(machine)for local ownership. - Prefer actor refs plus
useSelector(...)when you need shared ownership or selective updates. - Keep derived UI state close to the machine snapshot rather than duplicating it in separate stores.
Solid
- Use
useMachine(machine)for local ownership. - Prefer
useActorRef(...)anduseSelector(...)when an actor is shared or when you want tighter subscription control. - Keep signals derived from actor snapshot slices instead of rebuilding mode booleans manually.
Heuristic
Prefer useMachine(...) when:
- the actor is local to one component
- ownership is simple
- rendering pressure is low
Prefer actor ref + selector patterns when:
- the actor is shared across a subtree
- multiple components read different slices
- you want clearer ownership boundaries
- you want to avoid broad rerenders
Advanced XState v5 Patterns
Use this reference when the task involves any of these:
- typed actions beyond plain
assign(...) - deciding between
fromPromise(...)andfromCallback(...) - machine-emitted events
- persistence and hydration
- reducing over-modeled parent machines
Typed actions beyond assign(...)
LLMs often act like assign(...) is the only useful action. It is not.
For typed reusable action logic, prefer setup-scoped helpers from a const machineSetup = setup(...) object when they make the behavior clearer:
machineSetup.createAction(...)for typed custom actionsmachineSetup.enqueueActions(...)when one transition should enqueue several actions conditionallymachineSetup.emit(...)when the machine should emit an event to the surrounding systemmachineSetup.assign(...),machineSetup.raise(...),machineSetup.sendTo(...), etc. when you want type-bound built-ins
Minimal pattern:
import { setup } from 'xstate';
const machineSetup = setup({
types: {} as {
context: { count: number };
events: { type: 'count.incremented'; value: number } | { type: 'count.flushed' };
emitted: { type: 'count.changed'; count: number };
}
});
const increment = machineSetup.assign({
count: ({ context, event }) =>
event.type === 'count.incremented' ? context.count + event.value : context.count
});
const raiseFlush = machineSetup.raise({ type: 'count.flushed' });
const emitChanged = machineSetup.emit(({ context }) => ({
type: 'count.changed',
count: context.count
}));
const flushIfNeeded = machineSetup.enqueueActions(({ context, enqueue }) => {
if (context.count > 10) {
enqueue(raiseFlush);
}
});Local anchors:
- setup-scoped typed helpers: /Users/davidkpiano/Code/xstate/packages/core/CHANGELOG.md
enqueueActions(...)in a real machine: /Users/davidkpiano/Code/xstate/examples/tiles/src/tilesMachine.ts
Prefer event payloads over context relay
If state A only stores data so state B can use it immediately, ask whether that data should just travel on an event.
Prefer:
- event carries the payload the next transition or actor needs
- child actor input comes from the triggering event when that is the natural source
Be careful about:
- storing temporary request payloads in context solely so
onDoneor a later state can read them - adding context fields that duplicate transient protocol data
- storing raw transport artifacts like
latestChunk, callback payloads, or response wrappers when the machine only needs a derived durable value such asprogress
Context should hold durable state, not every hop in the protocol.
Choosing the right actor kind
Do not default to fromPromise(...).
Prefer fromPromise(...) when:
- one request produces one result or one failure
- the actor does not need to push multiple events over time
- the machine genuinely wants
onDone/onError
Prefer fromCallback(...) when:
- the actor wraps subscriptions, timers, DOM listeners, sockets, or external callbacks
- the actor needs to send multiple events back over time
- forcing everything into
onDone/onErrormakes the machine awkward
When writing callback-actor examples:
- declare the external callback API shape if it is not already defined in the snippet
- type the callback payload instead of leaving
msg,chunk, orerras implicitany - prefer sending domain events like
upload.progressortimer.tickedback to the parent - keep only durable results in context; avoid storing raw callback payloads unless the machine truly needs them later
Local anchors:
- callback actor with recurring events: /Users/davidkpiano/Code/xstate/examples/workflow-check-inbox/main.ts
- callback actor for ticking input: /Users/davidkpiano/Code/xstate/examples/stopwatch/src/stopwatchMachine.ts
Minimal upload-style callback pattern:
import { assign, fromCallback, setup } from 'xstate';
type UploadMessage =
| { type: 'progress'; loaded: number; total: number }
| { type: 'complete' }
| { type: 'error'; error: unknown };
declare function startUploadAndListen(
uploadId: string,
onMessage: (message: UploadMessage) => void
): () => void;
const machineSetup = setup({
types: {} as {
context: {
uploadId: string | null;
progress: number;
error: string | null;
};
events:
| { type: 'upload.started'; uploadId: string }
| { type: 'upload.progress'; loaded: number; total: number }
| { type: 'upload.completed' }
| { type: 'upload.failed'; error: string };
},
actors: {
upload: fromCallback<
{ type: 'upload.progress'; loaded: number; total: number }
| { type: 'upload.completed' }
| { type: 'upload.failed'; error: string },
{ uploadId: string }
>(({ input, sendBack }) => {
return startUploadAndListen(input.uploadId, (message) => {
if (message.type === 'progress') {
sendBack({
type: 'upload.progress',
loaded: message.loaded,
total: message.total
});
return;
}
if (message.type === 'complete') {
sendBack({ type: 'upload.completed' });
return;
}
sendBack({ type: 'upload.failed', error: String(message.error) });
});
})
}
});
export const uploadMachine = machineSetup.createMachine({
context: {
uploadId: null,
progress: 0,
error: null
},
initial: 'idle',
states: {
idle: {
on: {
'upload.started': {
target: 'uploading',
actions: assign({ uploadId: ({ event }) => event.uploadId })
}
}
},
uploading: {
invoke: {
src: 'upload',
input: ({ context }) => ({ uploadId: context.uploadId! })
},
on: {
'upload.progress': {
actions: assign({
progress: ({ event }) =>
Math.round((event.loaded / event.total) * 100)
})
},
'upload.completed': { target: 'done' },
'upload.failed': {
target: 'failed',
actions: assign({ error: ({ event }) => event.error })
}
}
},
done: {},
failed: {}
}
});Emitted events
Machines can emit events. This is useful when the machine is driving a larger system and should announce something meaningful outward, instead of forcing every consumer to poll context or infer behavior indirectly.
Prefer emitted events when:
- the machine is modeling a process that should notify the outside world
- the notification is part of the domain, not just UI plumbing
- the alternative is stuffing extra bookkeeping into context for others to watch
Use types.emitted plus machineSetup.emit(...) for typed emitted events.
Local anchor:
- typed emitted events and
machineSetup.emit(...): /Users/davidkpiano/Code/xstate/packages/core/CHANGELOG.md
Avoid over-modeling the parent machine
LLMs often over-expand the statechart.
Prefer coarse parent states when:
- the parent only cares about a higher-level mode
- an invoked/spawned child can own transient internal detail
- extra parent states mainly represent implementation noise
Do not split aggressively just for conceptual purity. The goal is a sturdier model, not the maximum number of actors and states.
Persistence and hydration
Prefer snapshot persistence over hand-rolled state reconstruction.
Canonical pattern:
import { createActor } from 'xstate';
import { authMachine } from './authMachine';
const persisted = loadPersistedSnapshot();
const actor = createActor(authMachine, {
snapshot: persisted
});
actor.subscribe(() => {
savePersistedSnapshot(actor.getPersistedSnapshot());
});In UI adapters, prefer provider or hook options that accept a persisted snapshot when available rather than rebuilding state manually.
Be careful about older examples that use state: in actor options. For current v5 code, prefer snapshot: when hydrating persisted machine state.
For migration or repair tasks, do not keep dead implementation override objects alive by passing them into useMachine(...) if the current hook does not accept them. Either move real implementations into setup(...)/machine config or remove dead overrides if they are unused.
Local anchors:
- persisted snapshot save/restore flow: /Users/davidkpiano/Code/xstate/examples/mongodb-persisted-state/main.ts
- persisted snapshot in an API workflow example: /Users/davidkpiano/Code/xstate/examples/express-workflow/index.ts
- React adapter tests using
snapshothydration: /Users/davidkpiano/Code/xstate/packages/xstate-react/test/createActorContext.test.tsx
XState v5 Examples
This file holds expandable canonical examples for the skill. Keep the examples opinionated and small enough to be reusable, but grow this file over time as recurring patterns emerge.
Machine with setup(...)
import { assign, assertEvent, setup } from 'xstate';
export const feedbackMachine = setup({
types: {} as {
context: {
feedback: string;
};
events:
| { type: 'feedback.good' }
| { type: 'feedback.bad' }
| { type: 'feedback.changed'; value: string }
| { type: 'feedback.submitted' }
| { type: 'feedback.restarted' };
},
guards: {
hasFeedback: ({ context }) => context.feedback.trim().length > 0
},
actions: {
updateFeedback: assign({
feedback: (_, params: { value: string }) => params.value
}),
clearFeedback: assign({
feedback: () => ''
}),
logSubmission: ({ event }) => {
assertEvent(event, 'feedback.submitted');
console.log('submitted feedback');
})
}
}).createMachine({
context: {
feedback: ''
},
initial: 'prompt',
states: {
prompt: {
on: {
'feedback.good': {
target: 'thanks'
},
'feedback.bad': {
target: 'form'
}
}
},
form: {
tags: ['editable'],
on: {
'feedback.changed': {
actions: [
{
type: 'updateFeedback',
params: ({ event }) => ({ value: event.value })
}
]
},
'feedback.submitted': [
{
guard: { type: 'hasFeedback' },
target: 'thanks',
actions: [{ type: 'logSubmission' }]
}
]
}
},
thanks: {
tags: ['completed'],
on: {
'feedback.restarted': {
target: 'prompt',
actions: [{ type: 'clearFeedback' }]
}
}
}
}
});Shared actor with React
import { createActorContext } from '@xstate/react';
import { feedbackMachine } from './feedbackMachine';
const FeedbackContext = createActorContext(feedbackMachine);
export function FeedbackProvider(props: { children: React.ReactNode }) {
return <FeedbackContext.Provider>{props.children}</FeedbackContext.Provider>;
}
export function FeedbackForm() {
const actorRef = FeedbackContext.useActorRef();
const canSubmit = FeedbackContext.useSelector((snapshot) =>
snapshot.can({ type: 'feedback.submitted' })
);
const isEditable = FeedbackContext.useSelector((snapshot) =>
snapshot.hasTag('editable')
);
if (!isEditable) {
return null;
}
return (
<form
onSubmit={(event) => {
event.preventDefault();
actorRef.send({ type: 'feedback.submitted' });
}}
>
<textarea
onChange={(event) => {
actorRef.send({
type: 'feedback.changed',
value: event.target.value
});
}}
/>
<button disabled={!canSubmit}>Submit</button>
</form>
);
}Shared async auth actor with React
When one actor is shared across a subtree, prefer a provider plus useSelector(...).
If a named assign(...) action updates multiple properties, use one params object that contains every field that action needs.
// authMachine.ts
import { assign, assertEvent, fromPromise, setup } from 'xstate';
type User = { id: string; email: string; name: string };
type Session = { token: string; expiresAt: number };
export const authMachine = setup({
types: {} as {
context: {
user: User | null;
session: Session | null;
error: string | null;
};
events:
| { type: 'auth.login'; email: string; password: string }
| { type: 'auth.logout' }
| { type: 'auth.refresh' };
},
actors: {
login: fromPromise(
async ({ input }: { input: { email: string; password: string } }) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input)
});
if (!res.ok) {
throw new Error('Login failed');
}
return (await res.json()) as { user: User; session: Session };
}
),
refreshSession: fromPromise(async () => {
const res = await fetch('/api/auth/refresh', { method: 'POST' });
if (!res.ok) {
throw new Error('Refresh failed');
}
return (await res.json()) as { session: Session };
})
},
actions: {
setAuth: assign({
user: (_, params: { user: User; session: Session }) => params.user,
session: (_, params: { user: User; session: Session }) => params.session,
error: () => null
}),
setSession: assign({
session: (_, params: { session: Session }) => params.session
}),
setError: assign({
error: (_, params: { message: string }) => params.message
}),
clearAuth: assign({
user: () => null,
session: () => null,
error: () => null
})
}
}).createMachine({
id: 'auth',
context: {
user: null,
session: null,
error: null
},
initial: 'signedOut',
states: {
signedOut: {
on: {
'auth.login': { target: 'signingIn' }
}
},
signingIn: {
tags: ['loading'],
invoke: {
src: 'login',
input: ({ event }) => {
assertEvent(event, 'auth.login');
return {
email: event.email,
password: event.password
};
},
onDone: {
target: 'signedIn',
actions: [
{
type: 'setAuth',
params: ({ event }) => ({
user: event.output.user,
session: event.output.session
})
}
]
},
onError: {
target: 'signedOut',
actions: [
{
type: 'setError',
params: ({ event }) => ({
message: event.error instanceof Error ? event.error.message : 'Login failed'
})
}
]
}
}
},
signedIn: {
on: {
'auth.logout': {
target: 'signedOut',
actions: [{ type: 'clearAuth' }]
},
'auth.refresh': { target: 'refreshing' }
}
},
refreshing: {
tags: ['loading'],
invoke: {
src: 'refreshSession',
onDone: {
target: 'signedIn',
actions: [
{
type: 'setSession',
params: ({ event }) => ({
session: event.output.session
})
}
]
},
onError: {
target: 'signedOut',
actions: [
{
type: 'setError',
params: ({ event }) => ({
message: event.error instanceof Error ? event.error.message : 'Refresh failed'
})
},
{ type: 'clearAuth' }
]
}
}
}
}
});// AuthContext.tsx
import { createActorContext } from '@xstate/react';
import { authMachine } from './authMachine';
export const AuthContext = createActorContext(authMachine);
export function AuthProvider(props: { children: React.ReactNode }) {
return <AuthContext.Provider>{props.children}</AuthContext.Provider>;
}// Navbar.tsx
import { AuthContext } from './AuthContext';
export function Navbar() {
const actorRef = AuthContext.useActorRef();
const userName = AuthContext.useSelector((snapshot) => snapshot.context.user?.name ?? null);
const isSignedIn = AuthContext.useSelector((snapshot) => snapshot.matches('signedIn'));
return (
<nav>
{isSignedIn ? (
<>
<span>{userName}</span>
<button onClick={() => actorRef.send({ type: 'auth.logout' })}>Logout</button>
</>
) : (
<span>Signed out</span>
)}
</nav>
);
}// SettingsPage.tsx
import { AuthContext } from './AuthContext';
export function SettingsPage() {
const actorRef = AuthContext.useActorRef();
const user = AuthContext.useSelector((snapshot) => snapshot.context.user);
const isLoading = AuthContext.useSelector((snapshot) => snapshot.hasTag('loading'));
if (!user) {
return <p>Please sign in.</p>;
}
return (
<section>
<p>{user.email}</p>
<button
disabled={isLoading}
onClick={() => actorRef.send({ type: 'auth.refresh' })}
>
{isLoading ? 'Refreshing…' : 'Refresh session'}
</button>
</section>
);
}// BillingPage.tsx
import { AuthContext } from './AuthContext';
export function BillingPage() {
const session = AuthContext.useSelector((snapshot) => snapshot.context.session);
const isSignedIn = AuthContext.useSelector((snapshot) => snapshot.matches('signedIn'));
if (!isSignedIn || !session) {
return <p>Billing is unavailable while signed out.</p>;
}
return (
<section>
<p>Session expires at: {new Date(session.expiresAt).toLocaleString()}</p>
</section>
);
}// App.tsx
import { AuthProvider } from './AuthContext';
import { BillingPage } from './BillingPage';
import { Navbar } from './Navbar';
import { SettingsPage } from './SettingsPage';
export function App() {
return (
<AuthProvider>
<Navbar />
<SettingsPage />
<BillingPage />
</AuthProvider>
);
}Prefer @xstate/store for simple domains
Use a store when the domain is just event-based state updates without meaningful modes or actors.
import { createStore } from '@xstate/store';
export const preferencesStore = createStore({
context: {
theme: 'light' as 'light' | 'dark',
compactMode: false
},
on: {
'preferences.themeChanged': (
context,
event: { value: 'light' | 'dark' }
) => ({
...context,
theme: event.value
}),
'preferences.compactModeToggled': (context) => ({
...context,
compactMode: !context.compactMode
})
}
});Migration notes example
Before
const machine = createMachine({
schema: {
context: {} as { count: number },
events: {} as { type: 'inc' } | { type: 'dec' }
},
on: {
inc: {
cond: (context) => context.count < 10,
actions: assign({
count: (context) => context.count + 1
})
}
}
});After
const machine = setup({
types: {} as {
context: { count: number };
events: { type: 'inc' } | { type: 'dec' };
},
guards: {
canIncrement: ({ context }) => context.count < 10
},
actions: {
increment: assign({
count: ({ context }) => context.count + 1
})
}
}).createMachine({
context: {
count: 0
},
on: {
inc: [
{
guard: { type: 'canIncrement' },
actions: [{ type: 'increment' }]
}
]
}
});Observables And Inspection
Use this reference when the task involves any of these:
fromObservable(...)or RxJS-backed actors- inspection/debugging of actor systems
- browser inspector wiring in app code
Observable actors
Prefer fromObservable(...) when the actor naturally wraps a stream of values from an observable source.
Use it when:
- the source is already an observable
- values arrive over time
- the actor should subscribe/unsubscribe with actor lifecycle
Prefer fromCallback(...) instead when the source is a callback/listener API rather than an observable.
Local anchor:
- typed
fromObservable(...)example: /Users/davidkpiano/Code/xstate/packages/core/CHANGELOG.md
Minimal shape:
import { createActor, fromObservable } from 'xstate';
import { interval } from 'rxjs';
type Output = number;
type Input = { period?: number };
const tickerLogic = fromObservable<Output, Input>(({ input }) => {
return interval(input.period ?? 1000);
});
const actor = createActor(tickerLogic, {
input: { period: 500 }
});Inspection API
When debugging actor systems programmatically, use actor.system.inspect(...).
This is useful when:
- you need structured transition/event/snapshot visibility
- you want to log or capture inspection events in tests or tooling
- you want lower-level visibility than ordinary subscriptions
It accepts either a function or an observer and returns a subscription.
Local anchor:
actor.system.inspect(...)usage and unsubscribe behavior: /Users/davidkpiano/Code/xstate/packages/core/test/inspect.test.ts
Minimal shape:
import { createActor, createMachine } from 'xstate';
const actor = createActor(createMachine({}));
const sub = actor.system.inspect((inspectionEvent) => {
console.log(inspectionEvent.type);
});
actor.start();
sub.unsubscribe();Browser inspector
When the user wants visual debugging in an app, prefer @statelyai/inspect with createBrowserInspector(...).
Local anchor:
- React template wiring: /Users/davidkpiano/Code/xstate/templates/react-ts/src/App.tsx
Minimal shape:
import { useMachine } from '@xstate/react';
import { createBrowserInspector } from '@statelyai/inspect';
import { feedbackMachine } from './feedbackMachine';
const { inspect } = createBrowserInspector({
autoStart: false
});
export function Feedback() {
const [snapshot, send] = useMachine(feedbackMachine, {
inspect
});
return (
<button onClick={() => send({ type: 'restart' })}>
{snapshot.matches('closed') ? 'Restart' : 'Send'}
</button>
);
}Rule of thumb
fromPromise(...): one request, one resultfromCallback(...): callback/listener protocol, many eventsfromObservable(...): real observable streamactor.system.inspect(...): programmatic visibilitycreateBrowserInspector(...): visual/debug UI visibility
React Integration
Use this reference after the machine design is settled and you need React-specific wiring detail beyond references/adapters.md.
For the shared-actor pattern with createActorContext(...), see references/examples.md. For inspector wiring (createBrowserInspector(...)), see references/observables-and-inspection.md.
Hook surface
| Hook | Use when |
|---|---|
useMachine(machine, options?) | The component owns a local actor; lifecycle is tied to the component. |
useActor(actorRef) | You already have an actor ref (from a parent, prop, or context) and want [snapshot, send]. |
useActorRef(machine, options?) | You need to send events but do not want the component to re-render on snapshot changes. |
useSelector(actorRef, selector) | You have a shared actor ref and want to re-render only when a specific slice changes. |
Prefer useActorRef + useSelector over useActor when the actor is shared or when broad rerenders matter. useActor is best reserved for small child components that legitimately need [snapshot, send] against a passed-in ref.
Passing initial context via input
When the machine uses input to seed context, pass it through useMachine:
import { useMachine } from '@xstate/react';
import { assign, setup } from 'xstate';
const counterMachine = setup({
types: {} as {
context: { count: number };
input: { initialCount: number };
events: { type: 'count.incremented' };
}
}).createMachine({
context: ({ input }) => ({ count: input.initialCount }),
on: {
'count.incremented': {
actions: assign({ count: ({ context }) => context.count + 1 })
}
}
});
export function Counter({ initialCount }: { initialCount: number }) {
const [snapshot, send] = useMachine(counterMachine, {
input: { initialCount }
});
return (
<button onClick={() => send({ type: 'count.incremented' })}>
{snapshot.context.count}
</button>
);
}Prefer input over reading props inside assign(...). Context is seeded once from input; later prop changes should be modeled as events.
Matching nested states
snapshot.matches(...) accepts both dot-string and object forms for hierarchical states. Both are valid; pick whichever reads more clearly locally.
if (snapshot.matches('processing.validating')) { /* ... */ }
if (snapshot.matches({ processing: 'confirming' })) { /* ... */ }For parallel states, use the object form for the specific region you care about, and prefer tags when several regions should answer a single UI question like "is anything loading?".
Snapshot-driven UI
Prefer driving JSX directly from the snapshot and actor, not from duplicated local state:
function AuthFlow() {
const [snapshot, send] = useMachine(authMachine);
if (snapshot.hasTag('loading')) return <LoadingSpinner />;
if (snapshot.matches('authenticated')) return <Dashboard user={snapshot.context.user} />;
if (snapshot.matches('error')) {
return (
<ErrorDisplay
message={snapshot.context.error}
onRetry={() => send({ type: 'auth.retried' })}
/>
);
}
return (
<LoginForm
canSubmit={snapshot.can({ type: 'auth.login', email: '', password: '' })}
onSubmit={(email, password) => send({ type: 'auth.login', email, password })}
/>
);
}Reach for snapshot.can(...) for enablement checks on buttons and inputs, not extra booleans in context.
Custom hook pattern
When a component tree repeatedly pulls the same slices from a machine, a thin wrapper hook can keep the call sites small without duplicating machine truth. Keep the wrapper close to derived values and event helpers; do not let it accumulate component state.
import { useMachine } from '@xstate/react';
import { authMachine } from './authMachine';
export function useAuth() {
const [snapshot, send, actorRef] = useMachine(authMachine);
return {
isAuthenticated: snapshot.matches('signedIn'),
isLoading: snapshot.hasTag('loading'),
user: snapshot.context.user,
error: snapshot.context.error,
login: (email: string, password: string) =>
send({ type: 'auth.login', email, password }),
logout: () => send({ type: 'auth.logout' }),
actorRef,
snapshot
};
}Prefer this pattern only for locally owned actors. For shared actors, build the same ergonomics with createActorContext(...) plus useSelector(...) so that reads are selective.
Ownership heuristic
- Component-local actor:
useMachine(...). - Shared actor across a subtree:
createActorContext(...)+useSelector(...)(seereferences/examples.md). - Actor handed down as a prop or ref:
useActor(actorRef)for small leaves, oruseSelector(actorRef, ...)when rerender pressure matters. - Send-only component:
useActorRef(...)so the component does not subscribe.
XState v4 to v5 Quick Reference
Use this file when you encounter older XState code and need a fast translation path without turning the task into a full migration project.
Default to the smallest safe translation. Preserve local structure unless the user asked for broader normalization.
Core renames
cond->guardschema->typesservices->actorsinterpret(machine)->createActor(machine)
Prefer v5 argument style
Prefer destructured arguments:
guard: ({ context, event }) => context.count > 0instead of older positional signatures:
cond: (context, event) => context.count > 0Prefer setup(...)
For new code or explicit normalization, prefer:
const machine = setup({
actions: {
save: () => {
// ...
}
},
guards: {
isValid: ({ context }) => context.ready
},
actors: {
loadUser: fromPromise(async () => {
// ...
})
}
}).createMachine({
// ...
});This gives a single place for named actions, guards, actors, and delays.
For small migration tasks, you do not need to introduce setup(...) if a more local v5 translation is clearer and safer.
Transition object shapes
Prefer explicit transition objects:
on: {
submit: {
target: 'saving',
actions: [{ type: 'trackSubmit' }]
}
}If you are only fixing a local issue in an older file, preserve nearby style unless the user asked for migration.
Invocation
v4 code often uses older service patterns. In v5, prefer named actors:
invoke: {
src: 'loadUser'
}with implementations defined in setup({ actors: ... }).
Actor creation
Prefer:
const actor = createActor(machine);
actor.start();instead of:
const service = interpret(machine).start();Typing
Prefer types over schema:
types: {} as {
context: { count: number };
events: { type: 'increment' } | { type: 'reset' };
}Guard and action objects
Prefer object forms when the behavior is named or parameterized:
guard: { type: 'hasPermission', params: { role: 'admin' } }
actions: [{ type: 'track', params: { eventName: 'form.submitted' } }]Preserve scope
Do not force a full v4 -> v5 rewrite when the user asked for a small bug fix or local migration. Translate only the touched area unless:
- the current code is broken due to mixed concepts
- the user explicitly asked for migration
- a local refactor is the simplest safe fix
Related skills
How it compares
Pick xstate over plain workflow prompts when agent logic needs explicit states, guards, and visual diagrams.
FAQ
What agents can run xstate models?
xstate models are designed for Claude, Cursor, and other coding agents that can read visual finite-state machine definitions. State charts expose transitions and guards so agents execute predictable multi-step behaviors.
When should developers adopt xstate for agents?
Developers should adopt xstate when agent logic includes branching, retries, or human-in-the-loop steps that outgrow linear prompts. Finite-state machines make orchestration auditable and shareable across agent runtimes.