Skill
state-machine-designer
Model entity state transitions, guard conditions, side effects, and generate implementation code. Use when designing order flows, auth states, approval workflows, or any entity with distinct states and transition rules. Trigger on "state machine", "workflow states", "order flow", "status transitions".
From project-orchestratorInstall
1
Run in your terminal$
npx claudepluginhub vivekmano27/agent-orchestrator --plugin project-orchestratorTool Access
This skill is limited to using the following tools:
ReadWriteEditGrepGlob
Skill Content
State Machine Designer Skill
Output Format
Write the state machine spec to .claude/specs/[feature]/state-machine.md using this exact structure:
# State Machine: [Entity Name]
## States
| State | Description | Entry Action | Terminal? |
|-------|-------------|-------------|-----------|
| draft | Created but not submitted | — | No |
| pending_review | Submitted, awaiting reviewer | Notify reviewer via email | No |
| approved | Reviewer accepted | Send approval email, create audit log | No |
| rejected | Reviewer declined | Send rejection email with reason | No |
| completed | All post-approval work finished | Update metrics, archive | Yes |
| cancelled | User cancelled before completion | Release held resources | Yes |
## Transitions
| # | From | To | Trigger | Guard | Side Effects |
|---|------|----|---------|-------|-------------|
| 1 | draft | pending_review | user.submit() | All required fields non-empty | Create audit log entry |
| 2 | pending_review | approved | reviewer.approve() | Reviewer role = ADMIN or MANAGER | Send email, log |
| 3 | pending_review | rejected | reviewer.reject(reason) | reason.length > 0 | Send email with reason |
| 4 | rejected | draft | user.revise() | — | Clear previous rejection |
| 5 | approved | completed | system.markDone() | All child tasks resolved | Archive, update dashboard |
| 6 | draft | cancelled | user.cancel() | — | — |
| 7 | pending_review | cancelled | user.cancel() | — | Notify reviewer of cancellation |
## Mermaid Diagram
\```mermaid
stateDiagram-v2
[*] --> draft
draft --> pending_review : submit
draft --> cancelled : cancel
pending_review --> approved : approve
pending_review --> rejected : reject
pending_review --> cancelled : cancel
rejected --> draft : revise
approved --> completed : markDone
completed --> [*]
cancelled --> [*]
\```
TypeScript Implementation Pattern
Generate the implementation alongside the spec. Use a discriminated union + transition map — not a switch statement chain.
// src/state-machines/order-state.ts
// 1. Define states as a const enum for zero-cost at runtime
export const OrderState = {
DRAFT: 'draft',
PENDING_REVIEW: 'pending_review',
APPROVED: 'approved',
REJECTED: 'rejected',
COMPLETED: 'completed',
CANCELLED: 'cancelled',
} as const;
export type OrderStateValue = typeof OrderState[keyof typeof OrderState];
// 2. Define events as discriminated union
export type OrderEvent =
| { type: 'SUBMIT' }
| { type: 'APPROVE'; reviewerId: string }
| { type: 'REJECT'; reviewerId: string; reason: string }
| { type: 'REVISE' }
| { type: 'MARK_DONE' }
| { type: 'CANCEL' };
// 3. Guard functions — return string (error message) on failure, null on success
type Guard = (context: OrderContext, event: OrderEvent) => string | null;
const guards: Record<string, Guard> = {
allFieldsFilled: (ctx) =>
!ctx.title || !ctx.description ? 'Title and description are required' : null,
hasReviewerPermission: (ctx, event) => {
if (event.type !== 'APPROVE' && event.type !== 'REJECT') return 'Invalid event';
return ctx.reviewerRole === 'ADMIN' || ctx.reviewerRole === 'MANAGER'
? null
: 'Reviewer must be ADMIN or MANAGER';
},
rejectionHasReason: (_ctx, event) =>
event.type === 'REJECT' && event.reason.length === 0
? 'Rejection reason is required'
: null,
allTasksResolved: (ctx) =>
ctx.pendingTasks > 0 ? `${ctx.pendingTasks} tasks still pending` : null,
};
// 4. Transition map — single source of truth
interface Transition {
target: OrderStateValue;
guard?: Guard;
sideEffects?: Array<(ctx: OrderContext, event: OrderEvent) => Promise<void>>;
}
const transitions: Record<OrderStateValue, Partial<Record<OrderEvent['type'], Transition>>> = {
[OrderState.DRAFT]: {
SUBMIT: {
target: OrderState.PENDING_REVIEW,
guard: guards.allFieldsFilled,
sideEffects: [notifyReviewer, createAuditLog],
},
CANCEL: {
target: OrderState.CANCELLED,
},
},
[OrderState.PENDING_REVIEW]: {
APPROVE: {
target: OrderState.APPROVED,
guard: guards.hasReviewerPermission,
sideEffects: [sendApprovalEmail, createAuditLog],
},
REJECT: {
target: OrderState.REJECTED,
guard: guards.rejectionHasReason,
sideEffects: [sendRejectionEmail, createAuditLog],
},
CANCEL: {
target: OrderState.CANCELLED,
sideEffects: [notifyReviewerOfCancellation],
},
},
[OrderState.APPROVED]: {
MARK_DONE: {
target: OrderState.COMPLETED,
guard: guards.allTasksResolved,
sideEffects: [archiveOrder, updateDashboard],
},
},
[OrderState.REJECTED]: {
REVISE: {
target: OrderState.DRAFT,
sideEffects: [clearRejection],
},
},
// Terminal states — no transitions out
[OrderState.COMPLETED]: {},
[OrderState.CANCELLED]: {},
};
// 5. Transition executor
export async function transition(
currentState: OrderStateValue,
event: OrderEvent,
context: OrderContext,
): Promise<{ newState: OrderStateValue; error?: string }> {
const stateTransitions = transitions[currentState];
const t = stateTransitions?.[event.type];
if (!t) {
return {
newState: currentState,
error: `No transition from "${currentState}" on event "${event.type}"`,
};
}
if (t.guard) {
const guardError = t.guard(context, event);
if (guardError) {
return { newState: currentState, error: guardError };
}
}
// Execute side effects sequentially (order matters)
if (t.sideEffects) {
for (const effect of t.sideEffects) {
await effect(context, event);
}
}
return { newState: t.target };
}
Validation Checklist
Run these checks against every state machine before finalizing. All must pass.
| # | Check | How to Verify |
|---|---|---|
| 1 | No orphan states | Every state appears as a From or To in at least one transition (except the initial state which may only appear as To) |
| 2 | All transitions have source + target | No transition row has an empty From or To cell |
| 3 | At least one terminal state | At least one state has Terminal? = Yes (no outgoing transitions) |
| 4 | Reachability | Every non-initial state is reachable from the initial state via some sequence of transitions |
| 5 | No dead ends (unless terminal) | Every non-terminal state has at least one outgoing transition |
| 6 | Guards are testable | Every guard condition can be evaluated with data available in the context object — no external API calls in guards |
| 7 | Side effects are idempotent | If a side effect fails and the transition is retried, running the side effect again must not cause duplicates (e.g., use idempotency keys for emails) |
| 8 | Mermaid matches table | The Mermaid diagram has exactly the same states and transitions as the table — no extras, no missing |
Constraints
- Terminal states must have empty transition rows. If a state has
Terminal? = Yesbut also has outgoing transitions in the table, that is a spec error. Flag it. - Never use string literals for states in implementation code. Always use the const object (
OrderState.DRAFT) so typos are caught at compile time. - Guards must be pure functions. They take context + event and return a result. No database calls, no API calls, no side effects inside guards. If you need to check external state, load it into the context before calling
transition(). - Side effects execute after guard passes but before the state is persisted. If a side effect fails, the transition should roll back (state stays the same). Document this in the spec.
- Always generate the Mermaid diagram. Developers will paste it into GitHub PRs for visual review. A state machine without a diagram is incomplete.
Similar Skills
Stats
Parent Repo Stars0
Parent Repo Forks0
Last CommitMar 15, 2026