From harness-claude
Spawn and manage XState child actors for independent concurrent state machines communicating via message passing. Use for dynamic collections like file uploads, chat rooms, or player sessions.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Spawn and manage child actors for independent, concurrent state machines that communicate via message passing
Models concurrent, independent state regions active simultaneously in XState machines. Use for UIs with orthogonal concerns like bold/italic toggles or parallel processes like upload/validation.
Builds, tests, and debugs event-driven state machines with EventMachine Laravel package for declarative workflows, parallel states, child delegation, event sourcing, timers, and HTTP endpoints.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Spawn and manage child actors for independent, concurrent state machines that communicate via message passing
createMachine definitions in their own files.spawn inside assign actions to create child actors dynamically. Store actor references in context.spawnChild from setup() or the actors config with invoke for static actor creation.send (targeting a specific actor) or sendTo.sendParent or by reaching a final state.stop when they are no longer needed — leaked actors cause memory issues.// 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),
}),
},
},
},
},
});
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:
invoke — 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.spawn — creates an actor tied to the machine's lifetime. The actor persists across state transitions until explicitly stopped. Best for dynamic collections.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.
https://stately.ai/docs/actors