From harness-claude
Implements XState guards for conditional transition eligibility and actions for side effects like logging, API calls, and context updates with assign. For business rules in state machines.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Control transition eligibility with guards and execute side effects with entry, exit, and transition actions
Generates full TypeScript type safety for XState machines using typegen (v4) and setup pattern (v5). Provides autocompletion for states/events/actions; resolves errors in guards/actions.
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.
Control transition eligibility with guards and execute side effects with entry, exit, and transition actions
assign actions during transitionsguards (v5) or as cond strings (v4).actions in the machine config.assign for context updates — it is the only way to change context. Never mutate context directly.actions), state entry (entry), or state exit (exit).actions: ['logTransition', 'updateContext', 'notifyParent'].// 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) });
},
},
}
);
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:
assign — updates context (pure, processed by XState)send / sendTo — sends an event to an actor (including self)sendParent — sends an event to the parent machineraise — sends an event to the machine itself (processed in the same step)log — logs to console (useful for debugging)stop — stops a child actorXState 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.
https://stately.ai/docs/guards