state-machine-modeling — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited state-machine-modeling (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
What it is: State-machine modeling is the discipline of making one lifecycle's legal behavior explicit before implementation — what an entity can be, what can happen to it, which happenings are legal in which state, and what must be true before and after each transition. The whole point is to move the rules for "what can follow what" out of scattered if-blocks and into one inspectable model, so that the set of reachable conditions is a deliberate enumeration rather than the accidental product of every boolean someone added.
Mental model — five primitives:
(current state, event) to a next state or a deterministic rejection.Analogy: Treat the lifecycle like a railway switchboard — the current track segment and signal determine which next track is legal, while maintenance work and passenger notifications happen after the routing decision is made.
Why it exists: Without an explicit machine, lifecycle rules scatter across booleans, if branches, handlers, jobs, and UI checks. That implicit machine admits impossible combinations, makes retries ambiguous, and leaves future implementers guessing which transitions are legal.
What it is NOT: It is not broad domain discovery, persistence schema design, HTTP resource design, frontend state-location strategy, or post-failure debugging. Those skills compose with this one after the lifecycle model is clear.
Common misconception: A workflow engine, statechart library, or agent framework does not remove the need to model the lifecycle. It may run the model, persist progress, visualize it, or recover after crashes — but it does not decide the domain's legal states, guards, invariants, retries, or compensation semantics for you.
Define legal lifecycle behavior for a domain object, UI flow, background job, integration, agent workflow, or distributed process. Covers:
refunded order was once paid").State modeling prevents boolean sprawl. When a workflow has several flags that can combine into impossible conditions, the model is already a state machine — just an implicit and unsafe one. N independent booleans represent 2^N possible combinations, most of which are nonsense (isPaid && isRefunded && isPending); the discipline replaces them with one explicit lifecycle value whose variants are the handful of states that can actually occur.
States are durations; events are instants. A state describes a condition that holds for some span of time (submitting, paid, waiting_for_approval). An event or command is the instantaneous occurrence that may move the machine (submit.clicked, payment.authorized, approval.timed_out). This is why Submit is a bad state name and Submitting can be a good one.
The governing principle, attributed to Yaron Minsky, is make illegal states unrepresentable. Pursue it in three descending tiers — use the strongest your language and runtime allow:
status literal, Rust enums with per-variant fields, F#/OCaml variants, Kotlin sealed classes; the typestate pattern threads the state through the type so a method only exists on the state that allows it).(state, event) -> next state | rejected, so there is exactly one place that can change state and no other code path can produce an illegal combination.Separate decision from effect. The transition decision should be pure and replayable: given the same state, event, and guard inputs, it returns the same next state or rejection. Side effects happen after the transition decision is accepted, carry idempotency keys where retries are possible, and are attached deliberately to entry, exit, or the transition itself.
pending_payment, paid, submitting, cancelled) over instantaneous verbs (pay, ship, submit). Keep them mutually exclusive. Mark the initial state and terminal states.refunded order was once paid").payment.timed_out), not as ambient if (now > deadline) checks.state x event pair, record a next state, self-transition, internal action, or explicit rejection reason. Blank cells are the bug: every event in every state is either handled or deliberately refused.approve only fires when amount <= limit). Guards must be pure predicates over known state, event data, clock inputs, permissions, or already-loaded facts.switch statements.The transition matrix is the central verification artifact. It turns vague lifecycle prose into a complete decision table — every (state x event) cell is handled or explicitly refused.
| Current state | Event / command | Guard | Next state | Action | Rejection if guard/state invalid |
|---|---|---|---|---|---|
pending_payment | payment.authorized | amount matches order total | paid | reserve inventory | reject if order cancelled |
pending_payment | payment.timed_out | deadline passed | cancelled | release hold | reject if already paid |
paid | refund.requested | refund window open | refund_pending | start refund | reject with refund_window_closed |
shipped | cancel.requested | none | shipped | none | reject with cannot_cancel_shipped |
Rules:
A flat finite state machine is the right tool until state explosion forces a richer formalism. The trigger: when independent concerns multiply. Three independent binary axes — online/offline and idle/syncing and authed/anon — already imply 2 x 2 x 2 = 8 flat states, and each new axis doubles it. When the state count grows multiplicatively or the same transition is duplicated across many states, lift to a statechart (David Harel, 1987 — the formalism behind UML state machines and XState).
| Symptom | Statechart feature | Why |
|---|---|---|
| The same transition is repeated on many child states | Hierarchical / nested state | Define the transition on the parent superstate once — handle cancel on Active, not on each sub-state. Cuts duplicated transitions. |
Independent axes multiply states (online/offline x syncing/idle x authed/anon) | Orthogonal / parallel regions | Model each axis as a concurrent region that holds state additively — 8 multiplicative states become 2 + 2 + 2 = 6. The primary cure for boolean-product explosion. |
| A user leaves and returns to a nested flow | History state | Re-enter a superstate at the sub-state it was last in, without tracking that yourself (resume an interrupted wizard where the user left off). |
| Entry/cleanup work is duplicated across inbound/outbound edges | Entry/exit actions | Attach the effect to the state, inherited down the hierarchy, not to every edge. |
Reach for a statechart formalism or library — XState v5 is the dominant JS/TS implementation, SCXML is the W3C interchange standard, UML state machines are the diagram form — rather than hand-rolling hierarchy and parallelism, which are exactly the features hand-written switch statements get wrong. Stay flat when the state set is small and single-axis; the statechart is overhead you only earn when explosion or transition-duplication appears.
A running machine can be treated as an actor: it owns internal state, receives events, emits events or snapshots, and processes messages through its transition logic. This is useful when a lifecycle decomposes into independent machines — parent/child UI flows, worker pools, background jobs, distributed process managers, or agent workflows. The actor boundary is ownership: other actors send events; they do not reach in and mutate state.
Use state-bound invocations for async work whose lifetime is tied to a state. Entering loading may invoke a fetch actor; leaving loading stops or cancels that actor, and any completion/error event is handled explicitly. This prevents ghost side effects — a response from a request started in loading arriving after the user cancelled and flipping a now-cancelled entity back to loaded. Use spawned or durable child actors only when work must outlive the state that started it, and then model the stop, cancellation, stale-result, and compensation paths. (Invoked actors are also a decomposition lever: when one machine grows unwieldy, split a self-contained concern into its own machine the parent invokes, rather than inlining its states.)
Modeling the lifecycle is independent of how you run it. Decide deliberately:
| Tier | Use when | Realize as | Key risk |
|---|---|---|---|
| In-process type | State lives inside one request, render, or local computation and never outlives the process | Discriminated union / sealed class / enum + an exhaustive transition function. (Frontend UI flow → see state-management for where the state lives; this skill for how it transitions.) | Runtime events from boundaries still need validation |
| Persisted transition guard | State outlives a request but the workflow is short and stays in one service | A status (+ version/fencing) column, a transition table, and a guarded write | Race conditions and stale events if updates are not atomic |
| Event-sourced aggregate | You need an audit trail and state can be derived from accepted events | Fold the event stream to current state — the fold is the transition function | Event design, snapshots, projections, and query models belong to neighboring skills (event-storming / entity-relationship-modeling) |
| Statechart / runtime library | UI/business logic needs hierarchy, parallel regions, visualization, model-based tests, or state-bound invocations | XState, an SCXML-compatible runtime, a UML/statechart tool, or equivalent | Overhead if the lifecycle is actually simple |
| Actor system / communicating machines | Independent machines own separate state and coordinate through events | Parent/child state-machine actors, an actor runtime, a process manager, or message-driven machines | Hidden shared state, unmodeled message contracts, orphaned child work |
| Durable execution | The workflow is long-running, distributed, crash-survivable, or human-in-the-loop | Temporal, AWS Step Functions, DBOS, Restate, Dapr Workflow, Inngest, a LangGraph-style durable graph, or an orchestrated saga / process manager | Engine versioning, determinism, idempotent activities, in-flight instance migration |
Upstream-displacement check. For the long-running, distributed, crash-survivable case, a hand-rolled persisted state machine is increasingly the wrong implementation. Durable-execution engines automatically persist not just the state but the execution position — every local variable, loop counter, and branch — and resume on different infrastructure exactly where a crash left off, eliminating the plumbing (DB writes per transition, switch/case dispatch, timeout scheduling, recovery logic) that hand-rolled machines rarely get fully correct. Temporal's own framing is that this lets you "eliminate or avoid state machines altogether" at the implementation layer. DBOS checkpoints workflow progress; the OpenAI Agents SDK documents durable integrations with Dapr, Temporal, Restate, and DBOS; LangGraph positions itself as infrastructure for long-running stateful agents. This does not displace this skill: the conceptual lifecycle — the states, the legal transitions, the guards, the invariants, the idempotency rules, the compensation steps — is exactly what you still design here and then hand to the engine. A saga coordinates a distributed transaction and compensates failed steps (the saga itself carries no state); a process manager is a state machine that drives that coordination by reacting to events plus current state. Model the lifecycle; then pick the tier.
An LLM agent loop is a state machine whether or not you model it as one: it occupies a step, an event (a tool result, a user reply, a model turn) drives the next step, and some condition terminates it. The recurring failure is letting the prompt / conversation history be the implicit state store — "where the agent is" is inferred by re-reading the transcript each turn. History is lossy, unbounded, token-expensive, and non-deterministic, so the agent re-does completed steps, skips required ones, and has no crash-resumable position. Apply the same discipline:
Anthropic's workflow/agent distinction reinforces the boundary: predefined, known code paths should be workflows (deterministic transition authority); open-ended tasks where the number of steps can't be hardcoded need agents with guardrails, checkpoints, and stopping conditions. Frameworks encode exactly this shape — LangGraph models the agent as a graph with checkpointed state; the OpenAI Agents SDK runs a turn loop with explicit run/stopping conditions; Temporal/DBOS supply durable execution for the orchestration. Keep transition authority deterministic when the path is safety-critical; let the model make judgments inside explicit guard/action slots, but do not let prompt text become the only state store.
Once a machine outlives a single process — a persisted status column, a queue-driven worker, a durable workflow — the network stops cooperating and the transition function must defend itself.
| Pattern | Model it as | Check |
|---|---|---|
| Retry | A retryable state + attempt count + next scheduled event | Max attempts and terminal failure are explicit |
| Timeout | A scheduled *.timed_out event | Timeout fires once and is idempotent |
| Cancellation | A cancel.requested event with state-specific legality | Cancellation after a terminal state is rejected or idempotent |
| Compensation | Forward states + compensating states/events | Compensation can itself fail and has its own terminal path |
| State-bound invocation | An invoked async actor tied to a state lifetime | Exit cancels/stops the work or makes a late completion deterministic |
| Saga / process manager | An orchestrator state machine driving local transactions | Each local step has continuation and compensation behavior |
| Duplicate delivery | Event identity + idempotency key | Same event replay returns the same result or a deterministic duplicate response |
| Concurrent / stale update | A version / fencing token at the transition authority | A stale transition cannot overwrite newer state |
| Machine-definition change | A machine version on long-running instances | In-flight instances remain executable after a deploy |
The reasoning behind the table:
paid to an already-paid order is a no-effect acknowledgement, never a second charge. Carry an idempotency key on the command so the receiver dedupes by identity rather than guessing from state.getVersion and Worker Versioning) for exactly this; a hand-rolled persisted machine must version its definition and record the migration policy, or in-flight instances silently take transitions they were never designed for.These are why a hand-rolled persisted machine is hard to get right, and why the durable-execution tier is often the better home for the long-running, distributed case.
state x event pair is handled or explicitly rejected.system-interface-contracts; test-level choices are verified with testing-strategy; frontend state-location questions are out of scope and route to state-management.| Anti-pattern | Why it fails | Fix |
|---|---|---|
Boolean sprawl — several is* flags that combine | 2^N representable combinations, most illegal; nothing prevents isPaid && isRefunded. | Collapse to one closed lifecycle state + per-state data. |
Stringly-typed status with no guard — a free status string mutated anywhere | Any code path can set any value; no legal-transition enforcement. | Route all writes through one transition function; make the type a closed union. |
State names as event verbs — Submit instead of Submitting | Confuses instantaneous events with duration states. | Name states as the condition the entity is in; name events as what happened. |
| Guard hidden inside an action | The transition looks legal until the side effect fails. | Make the guard an explicit predicate before the action. |
| Effects on every transition — same side effect duplicated on each inbound edge | Drifts out of sync; one edge eventually forgets it. | Move it to the state's entry/exit action. |
| Ghost side effect — an async effect outlives the state that launched it and resolves after the machine moved on | A stale result mutates a state it no longer belongs to. | Bind the effect to its state as an invoked actor: start on entry, cancel on exit; discard a late result. |
| Silent no-op on an unexpected event | The bug hides — the workflow stalls with no signal. | Reject with an observable error, route to a trap state, or acknowledge idempotently — never swallow it. |
Treating a duplicate/replayed event as new — second paid charges twice | At-least-once delivery makes redelivery normal; a non-idempotent transition corrupts state. | Make the transition idempotent and dedupe by idempotency key; guard stale writes with an expected-version / fencing token. |
Ambient timeout checks — if (now > deadline) scattered in handlers | Timeout behavior is unmodeled and untestable. | Model the timeout as a scheduled event with its own transition. |
| Flattened parallel concerns — independent axes enumerated as one state set | State count grows multiplicatively. | Use orthogonal regions or separate machines. |
Hand-rolled hierarchy — nested switch emulating superstates/parallelism | Exactly where hand-written machines get hierarchy and concurrency wrong. | Use a statechart formalism/library (XState, SCXML, UML). |
| Shared actor internals — one machine reaches into another's state | Breaks ownership; coupling and races. | Treat machines as actors with event contracts and owned internal state. |
| Hand-rolled durable workflow — manual DB-persisted machine for a long, distributed, crash-prone process | Recovery, execution-position, and timeout plumbing are rarely fully correct. | Hand the modeled lifecycle to a durable-execution engine or orchestrated saga. |
| Prompt as the state store — an LLM agent loop tracks "where it is" implicitly in the conversation history | History is lossy, unbounded, and non-deterministic; the agent re-does/skips steps and can't resume after a crash. | Store machine state externally; let the model act only through explicit transitions — the transcript is the event log, not the state. |
| Golden-hammer FSM — a trivial independent toggle gets a full transition framework | Ceremony with no guards, invariants, or invalid transitions to protect. | Use a boolean or small enum until lifecycle legality, retries, hierarchy, or invariants justify a machine. |
Reach for these when a decision needs more than this skill carries:
onentry/onexit, history states, and transition selection. https://www.w3.org/TR/scxml/| Use instead | When |
|---|---|
event-storming | You need to discover the broader domain flow, commands, policies, actors, aggregates, and events before a lifecycle is known. |
entity-relationship-modeling | You need persistence schema, keys, constraints, indexes, query shape, retention, or migration design for state data. |
api-design | You need HTTP routes, request/response shapes, status codes, headers, pagination, or endpoint versioning. |
state-management | You are deciding where frontend state lives and who owns it (server/client/URL/persistent) rather than modeling a lifecycle's legal transitions. |
observability-modeling | The lifecycle is settled and you need telemetry semantics, metrics, logs, traces, or alerts. |
debugging | A stateful system has already failed and needs reproduction, evidence capture, and root-cause analysis. |
<!-- skill-graph-context:start (generated — do not edit by hand) -->
Classification
software-architecturetruemodeling/state-machinesWhen to use
Not for
Related skills
system-interface-contracts, testing-strategyobservability-modeling, api-design, debugging, event-storming, system-interface-contracts, testing-strategy, state-managementKeywords
state machine, statechart, lifecycle states, transitions, guards, finite state machine, invalid states, status field, workflow invariants, state explosion<!-- skill-graph-context:end -->
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.