From xstate
Covers XState v5 action types and side-effect patterns. Use when implementing entry/exit actions, assign() for context updates, raise(), sendTo(), enqueueActions(), or deciding where to place effects. Includes type-bound action helpers (v5.22+).
How this skill is triggered — by the user, by Claude, or both
Slash command
/xstate:xstate-actions-and-effectsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Actions are **fire-and-forget** side effects. They run once and the machine does not wait for them.
Actions are fire-and-forget side effects. They run once and the machine does not wait for them.
Actions can be placed in three locations:
states: {
active: {
// 1. Entry — runs when entering this state
entry: [{ type: 'logEnter' }],
// 2. Exit — runs when leaving this state
exit: [{ type: 'logExit' }],
on: {
CLICK: {
target: 'next',
// 3. Transition — runs during the transition
actions: [{ type: 'handleClick' }],
},
},
},
}
On a transition from state A to state B:
Define in setup(), reference by string or object:
const machine = setup({
actions: {
logEvent: () => console.log('Event occurred'),
track: (_, params: { category: string; action: string }) => {
analytics.track(params.category, params.action);
},
},
}).createMachine({
entry: [
// String shorthand
'logEvent',
// Object form with params
{ type: 'track', params: { category: 'page', action: 'view' } },
],
});
const machine = setup({
actions: {
track: (_, params: { value: number }) => {
analytics.track('count', params.value);
},
},
}).createMachine({
context: { count: 0 },
entry: {
type: 'track',
params: ({ context }) => ({ value: context.count }),
},
});
const testMachine = machine.provide({
actions: {
track: () => { /* mock implementation */ },
},
});
Updates context immutably. Do not call assign() inside inline functions — it returns an action object, not imperative code.
import { assign } from 'xstate';
on: {
INCREMENT: {
actions: assign({
count: ({ context }) => context.count + 1,
}),
},
'item.add': {
actions: assign({
items: ({ context, event }) => [...context.items, event.item],
}),
},
}
on: {
RESET: {
actions: assign(({ context }) => ({
count: 0,
items: [],
error: null,
})),
},
}
// BAD — mutation, causes bugs
assign({ items: ({ context }) => { context.items.push(x); return context.items; } })
// GOOD — immutable
assign({ items: ({ context }) => [...context.items, x] })
Sends an event to the same machine (internal event queue):
import { raise } from 'xstate';
// Static event
entry: raise({ type: 'INTERNAL_EVENT' }),
// Dynamic event
entry: raise(({ context }) => ({
type: 'PROCESS',
data: context.pendingData,
})),
// Delayed raise
entry: raise({ type: 'TIMEOUT' }, { delay: 5000 }),
Raised events are processed after the current transition completes, before external events.
Sends an event to a specific actor:
import { sendTo } from 'xstate';
on: {
FORWARD: {
// To actor by ID
actions: sendTo('childActor', { type: 'PROCESS' }),
},
NOTIFY: {
// Dynamic event
actions: sendTo('logger', ({ context }) => ({
type: 'LOG',
message: `Count: ${context.count}`,
})),
},
DELAYED: {
// With delay and cancellation ID
actions: sendTo('worker', { type: 'START' }, { delay: 1000, id: 'delayedStart' }),
},
}
Dynamic target (actor ref from context):
actions: sendTo(
({ context }) => context.parentRef,
{ type: 'CHILD_DONE' },
),
Prefer sendTo() over sendParent() — pass the parent ref via input for loose coupling. See the xstate-actors-and-invocation skill for the pattern.
Conditional action execution — replaces the deprecated choose():
import { enqueueActions } from 'xstate';
entry: enqueueActions(({ context, event, enqueue, check }) => {
// Always enqueue
enqueue.assign({ lastEvent: event.type });
// Conditional
if (context.count > 10) {
enqueue({ type: 'notifyHighCount' });
}
// Guard-based condition
if (check({ type: 'isAdmin' })) {
enqueue.sendTo('adminPanel', { type: 'REFRESH' });
}
// Built-in helpers
enqueue.raise({ type: 'INTERNAL' });
enqueue.assign({ processed: true });
enqueue.sendTo('logger', { type: 'LOG' });
}),
const machine = setup({
actions: {
processItems: enqueueActions(({ enqueue }, params: { batchSize: number }) => {
for (let i = 0; i < params.batchSize; i++) {
enqueue({ type: 'processItem' });
}
}),
},
}).createMachine({
entry: { type: 'processItems', params: { batchSize: 5 } },
});
Warning: Do NOT use async inside enqueueActions. Actions must be synchronous.
import { log } from 'xstate';
entry: log(({ context }) => `Entered with count: ${context.count}`),
Cancels a delayed sendTo() or raise() by ID:
import { cancel } from 'xstate';
on: {
ABORT: {
actions: cancel('delayedStart'), // Cancels the delayed action with this ID
},
}
import { stopChild } from 'xstate';
on: {
STOP_WORKER: {
actions: [
stopChild('workerId'),
assign({ workerRef: undefined }), // Clean up reference
],
},
}
import { spawnChild } from 'xstate';
on: {
START_WORKER: {
actions: spawnChild('workerLogic', {
id: 'worker',
input: ({ context }) => ({ data: context.data }),
}),
},
}
Create fully typed actions outside the machine definition. Enables splitting large machines across files.
import { setup } from 'xstate';
const machineSetup = setup({
types: {
context: {} as { count: number; items: string[] },
events: {} as { type: 'increment' } | { type: 'addItem'; item: string },
emitted: {} as { type: 'COUNT_CHANGED'; count: number },
},
});
// Type-bound assign — can be in a separate file
const incrementCount = machineSetup.assign({
count: ({ context }) => context.count + 1,
});
const addItem = machineSetup.assign({
items: ({ context, event }) => [...context.items, event.item],
});
// Type-bound raise
const raiseIncrement = machineSetup.raise({ type: 'increment' });
// Type-bound emit
const emitCountChanged = machineSetup.emit(({ context }) => ({
type: 'COUNT_CHANGED',
count: context.count,
}));
// Custom action with createAction
const logState = machineSetup.createAction(({ context, event }) => {
console.log("Count: " + context.count + ", Event: " + event.type);
});
// Type-bound enqueueActions
const batchUpdate = machineSetup.enqueueActions(({ enqueue }) => {
enqueue(incrementCount);
enqueue(logState);
});
// Use in machine — all fully typed
const machine = machineSetup.createMachine({
context: { count: 0, items: [] },
initial: 'active',
states: {
active: {
entry: [incrementCount, logState],
on: {
increment: { actions: [incrementCount, emitCountChanged] },
addItem: { actions: addItem },
},
},
},
});
Available type-bound helpers:
machineSetup.assign()machineSetup.raise()machineSetup.emit()machineSetup.sendTo()machineSetup.log()machineSetup.cancel()machineSetup.spawnChild()machineSetup.stopChild()machineSetup.enqueueActions()machineSetup.createAction()// BAD — assign() returns an object, doesn't execute imperatively
entry: ({ context }) => {
assign({ count: context.count + 1 }); // Does nothing!
},
// GOOD — use assign directly
entry: assign({ count: ({ context }) => context.count + 1 }),
// GOOD — or use enqueueActions for imperative style
entry: enqueueActions(({ context, enqueue }) => {
enqueue.assign({ count: context.count + 1 });
}),
// BAD — async actions are NOT awaited
entry: async ({ context }) => {
const data = await fetchData(); // Machine doesn't wait for this!
},
// GOOD — use invoke for async operations
loading: {
invoke: {
src: 'fetchData',
onDone: { target: 'success', actions: assign({ data: ({ event }) => event.output }) },
onError: { target: 'error' },
},
}
// BAD — string reference with no implementation
entry: 'doSomething', // Will be a no-op if not provided
// GOOD — implement in setup or provide later
const machine = setup({
actions: {
doSomething: () => { /* implementation */ },
},
}).createMachine({
entry: { type: 'doSomething' },
});
npx claudepluginhub ilyagulya/claude-marketplace --plugin xstateGenerates full type safety for XState machines using the v5 setup pattern or v4 typegen. Helps with TypeScript errors on event types, guards, and actions.
Builds, tests, and debugs event-driven state machines in Laravel using EventMachine. Activates on state machine definitions, behaviors, test assertions, parallel states, delegation, timers, and endpoints.
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.