install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/xstate-actor-pattern" ~/.claude/skills/intense-visions-harness-engineering-xstate-actor-pattern && rm -rf "$T"
manifest:
agents/skills/claude-code/xstate-actor-pattern/SKILL.mdsource content
XState Actor Pattern
Spawn and manage child actors for independent, concurrent state machines that communicate via message passing
When to Use
- Managing a dynamic collection of independent entities (chat rooms, file uploads, player sessions)
- When a child workflow has its own lifecycle but the parent needs to coordinate
- Replacing shared mutable state between concurrent processes with message passing
- Building systems where components can be added/removed at runtime
Instructions
- Define child machines as standalone
definitions in their own files.createMachine - In XState v4, use
insidespawn
actions to create child actors dynamically. Store actor references in context.assign - In XState v5, use
fromspawnChild
or thesetup()
config withactors
for static actor creation.invoke - Send events to child actors with
(targeting a specific actor) orsend
.sendTo - Child actors communicate back to the parent via
or by reaching a final state.sendParent - Clean up actors with
when they are no longer needed — leaked actors cause memory issues.stop
// upload.machine.ts — child machine for a single file upload import { createMachine, assign, sendParent } from 'xstate'; interface UploadContext { file: File; progress: number; } type UploadEvent = { type: 'PROGRESS'; percent: number } | { type: 'CANCEL' }; export const uploadMachine = createMachine<UploadContext, UploadEvent>({ id: 'upload', initial: 'uploading', states: { uploading: { invoke: { src: 'uploadFile', onDone: 'complete', onError: 'failed', }, on: { PROGRESS: { actions: assign({ progress: (_, e) => e.percent }) }, CANCEL: 'cancelled', }, }, complete: { type: 'final', entry: sendParent((ctx) => ({ type: 'UPLOAD_COMPLETE', file: ctx.file.name })), }, failed: { on: { RETRY: 'uploading' }, }, cancelled: { type: 'final' }, }, });
// uploader.machine.ts — parent machine that spawns upload actors import { createMachine, assign, spawn, ActorRefFrom } from 'xstate'; import { uploadMachine } from './upload.machine'; interface UploaderContext { uploads: Array<{ id: string; ref: ActorRefFrom<typeof uploadMachine> }>; } type UploaderEvent = | { type: 'ADD_FILE'; file: File } | { type: 'CANCEL_UPLOAD'; id: string } | { type: 'UPLOAD_COMPLETE'; file: string }; const uploaderMachine = createMachine<UploaderContext, UploaderEvent>({ id: 'uploader', initial: 'active', context: { uploads: [] }, states: { active: { on: { ADD_FILE: { actions: assign({ uploads: (ctx, event) => [ ...ctx.uploads, { id: event.file.name, ref: spawn(uploadMachine.withContext({ file: event.file, progress: 0 })), }, ], }), }, CANCEL_UPLOAD: { actions: (ctx, event) => { const upload = ctx.uploads.find((u) => u.id === event.id); upload?.ref.send({ type: 'CANCEL' }); }, }, UPLOAD_COMPLETE: { actions: assign({ uploads: (ctx, event) => ctx.uploads.filter((u) => u.id !== event.file), }), }, }, }, }, });
Details
Actor model basics: Each actor has its own state, processes messages sequentially, and communicates only via message passing. No shared memory. This eliminates race conditions by design.
invoke vs spawn:
— creates an actor tied to a specific state node. The actor starts when the state is entered and stops when the state is exited. Best for service calls with a clear lifecycle.invoke
— creates an actor tied to the machine's lifetime. The actor persists across state transitions until explicitly stopped. Best for dynamic collections.spawn
XState v5 actor types:
fromPromise, fromObservable, fromCallback, fromTransition, and child state machines. Each is a different actor "logic" type:
// v5 style const machine = setup({ actors: { fetchUser: fromPromise(async ({ input }: { input: { id: string } }) => { const res = await fetch(`/api/users/${input.id}`); return res.json(); }), }, }).createMachine({ /* ... */ });
Lifecycle management: Always clean up spawned actors. In v4, use
stop action. In v5, actors are garbage-collected when their parent stops, but explicit cleanup is still recommended for resource-heavy actors.
Testing actors: Test child machines in isolation first. Then test the parent machine's coordination logic separately. This keeps tests focused and fast.
Source
https://stately.ai/docs/actors
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.