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-guards-actions" ~/.claude/skills/intense-visions-harness-engineering-xstate-guards-actions && rm -rf "$T"
manifest:
agents/skills/claude-code/xstate-guards-actions/SKILL.mdsource content
XState Guards and Actions
Control transition eligibility with guards and execute side effects with entry, exit, and transition actions
When to Use
- Conditionally allowing or blocking state transitions based on context or event data
- Running side effects (logging, analytics, API calls) when entering or leaving states
- Updating context data via
actions during transitionsassign - Implementing business rules like "can only submit if form is valid"
Instructions
- Guards are pure boolean functions that determine whether a transition is allowed. Define them in the machine config under
(v5) or asguards
strings (v4).cond - Actions are fire-and-forget side effects. They run but their return values are ignored. Define them under
in the machine config.actions - Use
for context updates — it is the only way to change context. Never mutate context directly.assign - Place actions on transitions (
), state entry (actions
), or state exit (entry
).exit - Execution order: exit actions of source state, transition actions, entry actions of target state.
- Use arrays for multiple actions:
.actions: ['logTransition', 'updateContext', 'notifyParent'] - For conditional transitions (multiple targets from one event), use an array of guarded transitions evaluated in order.
// checkout.machine.ts import { createMachine, assign } from 'xstate'; interface CheckoutContext { items: Array<{ id: string; price: number }>; couponApplied: boolean; total: number; } type CheckoutEvent = | { type: 'ADD_ITEM'; item: { id: string; price: number } } | { type: 'APPLY_COUPON'; code: string } | { type: 'SUBMIT' } | { type: 'CONFIRM' }; const checkoutMachine = createMachine<CheckoutContext, CheckoutEvent>( { id: 'checkout', initial: 'cart', context: { items: [], couponApplied: false, total: 0 }, states: { cart: { on: { ADD_ITEM: { actions: ['addItem', 'recalculateTotal'], }, APPLY_COUPON: { actions: 'applyCoupon', guard: 'isValidCoupon', }, SUBMIT: [ // Guarded transitions — evaluated top to bottom { target: 'review', guard: 'hasItems' }, // Fallback — no guard { actions: 'showEmptyCartError' }, ], }, }, review: { entry: 'logReviewStep', on: { CONFIRM: { target: 'confirmed', guard: 'hasItems' }, }, }, confirmed: { type: 'final', entry: 'sendConfirmationEmail', }, }, }, { guards: { hasItems: (ctx) => ctx.items.length > 0, isValidCoupon: (ctx, event) => event.type === 'APPLY_COUPON' && event.code.startsWith('SAVE'), }, actions: { addItem: assign({ items: (ctx, event) => (event.type === 'ADD_ITEM' ? [...ctx.items, event.item] : ctx.items), }), recalculateTotal: assign({ total: (ctx) => { const subtotal = ctx.items.reduce((sum, item) => sum + item.price, 0); return ctx.couponApplied ? subtotal * 0.9 : subtotal; }, }), applyCoupon: assign({ couponApplied: true }), logReviewStep: () => console.log('Entered review step'), showEmptyCartError: () => console.warn('Cart is empty'), sendConfirmationEmail: (ctx) => { // Side effect — fire and forget fetch('/api/confirm', { method: 'POST', body: JSON.stringify(ctx) }); }, }, } );
Details
Guard evaluation order: When multiple transitions share the same event, guards are evaluated top to bottom. The first transition whose guard returns
true (or has no guard) is taken. This is an if/else-if chain.
assign is special:
assign returns an action object that XState processes internally. It is not a regular side effect — it is the mechanism for updating context. Never call assign conditionally inside a regular action function; use guarded transitions instead.
Action types:
— updates context (pure, processed by XState)assign
/send
— sends an event to an actor (including self)sendTo
— sends an event to the parent machinesendParent
— sends an event to the machine itself (processed in the same step)raise
— logs to console (useful for debugging)log
— stops a child actorstop- Custom functions — fire-and-forget side effects
XState v5 differences: Guards use
guard instead of cond. Actions and guards are defined in setup():
const machine = setup({ guards: { hasItems: ({ context }) => context.items.length > 0, }, actions: { addItem: assign({ /* ... */ }), }, }).createMachine({ /* ... */ });
Testing guards: Extract guard logic into standalone functions and unit test them. Test the machine integration separately.
Source
https://stately.ai/docs/guards
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.