RepoPilotOpen in app →

dotnet-state-machine/stateless

A simple library for creating state machines in C# code

Mixed

Mixed signals — read the receipts

worst of 4 axes
Use as dependencyConcerns

non-standard license (Other)

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 5w ago
  • 15 active contributors
  • Other licensed
Show 4 more →
  • CI configured
  • Tests present
  • Concentrated ownership — top contributor handles 70% of recent commits
  • Non-standard license (Other) — review terms
What would change the summary?
  • Use as dependency ConcernsMixed if: clarify license terms

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Forkable" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Forkable
[![RepoPilot: Forkable](https://repopilot.app/api/badge/dotnet-state-machine/stateless?axis=fork)](https://repopilot.app/r/dotnet-state-machine/stateless)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/dotnet-state-machine/stateless on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: dotnet-state-machine/stateless

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/dotnet-state-machine/stateless shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

WAIT — Mixed signals — read the receipts

  • Last commit 5w ago
  • 15 active contributors
  • Other licensed
  • CI configured
  • Tests present
  • ⚠ Concentrated ownership — top contributor handles 70% of recent commits
  • ⚠ Non-standard license (Other) — review terms

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live dotnet-state-machine/stateless repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/dotnet-state-machine/stateless.

What it runs against: a local clone of dotnet-state-machine/stateless — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in dotnet-state-machine/stateless | Confirms the artifact applies here, not a fork | | 2 | License is still Other | Catches relicense before you depend on it | | 3 | Default branch dev exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 65 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>dotnet-state-machine/stateless</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of dotnet-state-machine/stateless. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/dotnet-state-machine/stateless.git
#   cd stateless
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of dotnet-state-machine/stateless and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "dotnet-state-machine/stateless(\\.git)?\\b" \\
  && ok "origin remote is dotnet-state-machine/stateless" \\
  || miss "origin remote is not dotnet-state-machine/stateless (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(Other)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"Other\"" package.json 2>/dev/null) \\
  && ok "license is Other" \\
  || miss "license drift — was Other at generation time"

# 3. Default branch
git rev-parse --verify dev >/dev/null 2>&1 \\
  && ok "default branch dev exists" \\
  || miss "default branch dev no longer exists"

# 4. Critical files exist
test -f "src/Stateless/StateMachine.cs" \\
  && ok "src/Stateless/StateMachine.cs" \\
  || miss "missing critical file: src/Stateless/StateMachine.cs"
test -f "src/Stateless/StateRepresentation.cs" \\
  && ok "src/Stateless/StateRepresentation.cs" \\
  || miss "missing critical file: src/Stateless/StateRepresentation.cs"
test -f "src/Stateless/TriggerBehaviour.cs" \\
  && ok "src/Stateless/TriggerBehaviour.cs" \\
  || miss "missing critical file: src/Stateless/TriggerBehaviour.cs"
test -f "src/Stateless/StateConfiguration.cs" \\
  && ok "src/Stateless/StateConfiguration.cs" \\
  || miss "missing critical file: src/Stateless/StateConfiguration.cs"
test -f "src/Stateless/Reflection/StateMachineInfo.cs" \\
  && ok "src/Stateless/Reflection/StateMachineInfo.cs" \\
  || miss "missing critical file: src/Stateless/Reflection/StateMachineInfo.cs"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 65 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~35d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/dotnet-state-machine/stateless"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Stateless is a .NET library for building state machines and workflow engines directly in C# code using a fluent API. It provides first-class support for hierarchical states, parameterized triggers, guard conditions, entry/exit actions, and can export state diagrams to Mermaid or Graphviz DOT formats. The core abstraction is StateMachine<TState, TTrigger> which manages transitions between any .NET types (enums, strings, numbers) with declarative configuration. Standard single-project structure: /src/Stateless/ contains the core engine (StateMachine.cs implied), with behavior classes (EntryActionBehaviour.cs, ExitActionBehaviour.cs, etc.), /src/Stateless/Graph/ handles diagram export (Mermaid, UML Dot), and /example/ contains 5 runnable domain-specific demonstrations. No monorepo complexity.

👥Who it's for

.NET backend developers and enterprise architects building workflows, business process automation, and finite state machine logic (e.g., order processing pipelines, telephony call flows, bug tracking state transitions). Used by teams needing embeddable state logic without external workflow engines.

🌱Maturity & risk

Actively maintained and production-ready. The project has a GitHub Actions CI/CD pipeline (BuildAndTestOnPullRequests.yml), organized examples across multiple domains (AlarmExample, TelephoneCallExample, BugTrackerExample), a CHANGELOG, and clear documentation. The core API is stable with incremental features like Mermaid export being added.

Low risk for core functionality. Single language (C#-only), no evident external dependencies listed, and straightforward scope limit state machine concerns. Risk factors: relatively small community compared to alternatives like Akka.NET, and maintainer bus factor not visible from file list. No breaking changes evident in recent history based on organized CHANGELOG.

Active areas of work

Active feature development around graph export capabilities (MermaidGraph, UmlDotGraph with style support). Build pipeline validates on pull requests. Recent work appears focused on visualization and hierarchical state introspection rather than core engine changes.

🚀Get running

git clone https://github.com/dotnet-state-machine/stateless.git
cd stateless
dotnet restore Stateless.sln
dotnet build Stateless.sln
dotnet test  # if test project exists in solution

Daily commands: This is a library, not an executable. To experiment: cd example/TelephoneCallExample && dotnet run or cd example/OnOffExample && dotnet run. No web server or background service. For development: dotnet build src/Stateless/Stateless.csproj rebuilds the library; changes are reflected in example projects immediately.

🗺️Map of the codebase

  • src/Stateless/StateMachine.cs — Core state machine orchestrator—defines the primary API and state transition logic that all other components depend on
  • src/Stateless/StateRepresentation.cs — Internal representation of a state—manages entry/exit actions, trigger behaviors, and substates that drive state machine behavior
  • src/Stateless/TriggerBehaviour.cs — Abstract base for all trigger behaviors—determines how state machine responds to triggers and defines the behavior hierarchy
  • src/Stateless/StateConfiguration.cs — Fluent API for configuring states—used by nearly every example and test to set up transitions, guards, and actions
  • src/Stateless/Reflection/StateMachineInfo.cs — Introspection API exposing state machine structure—enables graph generation and analysis tools to understand the machine
  • src/Stateless/Graph/StateGraph.cs — Graph generation contract and base—core to producing Mermaid and UML visualizations of state machines

🛠️How to make changes

Add a new state with entry/exit actions

  1. Define state enum value in your state enum (e.g., State.NewState) (example/TelephoneCallExample/PhoneCall.cs)
  2. Call stateMachine.Configure(State.NewState) in setup to return a StateConfiguration builder (src/Stateless/StateConfiguration.cs)
  3. Chain fluent methods: .OnEntry(t => DoEntry()).OnExit(t => DoExit()).Permit(trigger, nextState) (src/Stateless/StateConfiguration.cs)
  4. For async entry/exit, use .OnEntryAsync(t => TaskAsync()) from the async variant (src/Stateless/StateConfiguration.Async.cs)

Add a guarded transition (conditional state change)

  1. In StateConfiguration, call .PermitIf(trigger, nextState, guardCondition) instead of .Permit() (src/Stateless/StateConfiguration.cs)
  2. Pass a Func<bool> guard predicate; if true, transition fires, else remains in current state (src/Stateless/TransitionGuard.cs)
  3. For parameterized guards, use PermitIf(triggerWithParams, nextState, (param) => condition(param)) (src/Stateless/TriggerWithParameters.cs)
  4. For async guards, use .PermitIfAsync() which accepts Func<Task<bool>> (src/Stateless/StateConfiguration.Async.cs)

Add internal transitions (action without state change)

  1. In StateConfiguration, call .InternalTransition(trigger, action) instead of .Permit() (src/Stateless/StateConfiguration.cs)
  2. The action callback executes within the state context—no entry/exit actions fire (src/Stateless/InternalTriggerBehaviour.cs)
  3. For parameterized internal transitions with type-safe parameters, use .InternalTransition(triggerWithParams, (param) => Action(param)) (src/Stateless/StateConfiguration.cs)
  4. For async internal actions, use .InternalTransitionAsync(trigger, t => TaskAsync()) (src/Stateless/StateConfiguration.Async.cs)

Add a parameterized trigger with type-safe arguments

  1. Create TriggerWithParameters<T> instance: new TriggerWithParameters<string>(Trigger.SomeTrigger) (src/Stateless/TriggerWithParameters.cs)
  2. In configuration, use .Permit(paramTrigger, nextState) or .PermitIf(paramTrigger, state, (param) => Guard(param)) (src/Stateless/StateConfiguration.cs)
  3. Fire the trigger with argument: stateMachine.Fire(paramTrigger, myValue); parameter flows to guard and entry actions (src/Stateless/StateMachine.cs)
  4. For async triggers, use FireAsync(paramTrigger, value) and async configuration methods (src/Stateless/StateMachine.Async.cs)

🔧Why these technologies

  • C# generics (TState, TTrigger) — Type safety for states and triggers; allows compile-time verification of valid transitions
  • Async/await patterns (Task-based API) — First-class async support for long-running entry/exit actions and guard conditions without blocking threads
  • Fluent builder pattern (StateConfiguration) — Readable, IDE-friendly DSL for non-programmers to define workflows; chains configuration calls
  • Reflection and introspection (StateMachineInfo) — Enables dynamic graph generation, visualization, and external tooling without modifying state machine
  • Abstract behavior hierarchy (TriggerBehaviour subclasses) — Extensible design allowing new trigger types (reentry, internal, dynamic) without modifying core engine

⚖️Trade-offs already made

  • Enum-based states and triggers

    • Why: Simple, type-safe, and requires no external database or metadata
    • Consequence: Cannot dynamically create new states/triggers at runtime; users must define enum statically
  • Fluent API over XML/JSON configuration

    • Why: Type-safe and refactoring-friendly; leverages C# language features
    • Consequence: State machine definition must be in code; no declarative external format (though introspection enables serialization)
  • Guard conditions as boolean Funcs instead of separate DSL

    • Why: Reuses C# language and avoids expression parser dependency
    • Consequence: Guards cannot be serialized/deserialized; stored as closures
  • Single-threaded synchronous state transitions by default

    • Why: Simpler mental model and fewer race conditions in typical workflows
    • Consequence: Concurrent firing of triggers must be serialized externally; no built-in lock or queue

🚫Non-goals (don't propose these)

  • Does not provide distributed/persistent state storage; state lives in memory only
  • Does not enforce thread safety for concurrent trigger firing; caller responsible for synchronization
  • Does not support hierarchical/nested state machines as a first-class primitive (composition only)
  • Does not include a workflow engine with persistence, escalation, or human task integration

🪤Traps & gotchas

No external configuration required. Library is self-contained. Beware state machine reentrance: transitions during OnEntry/OnExit actions can cause unexpected behavior—see examples for patterns. Graph export assumes directed acyclic transitions or will loop infinitely on cyclic state definitions without guards. Hierarchical states are not true substates: OnHold is logically a child of Connected but trigger permits must be explicitly configured on both—inheritance is behavioral, not automatic.

🏗️Architecture

💡Concepts to learn

  • Hierarchical State Machines (Nested States) — Stateless supports SubstateOf() to model real-world workflows where states contain sub-states; essential for OnHold being a substate of Connected without duplicating transition logic
  • Guard Clauses / Guard Conditions — Enables conditional transitions based on runtime data (e.g., 'only transition if balance > 0'); Stateless uses GuardCondition to evaluate permissions before firing triggers
  • Entry/Exit Actions — Stateless executes OnEntry/OnExit callbacks when entering/leaving states; critical for side effects like starting timers or logging without polluting state machine logic
  • Parameterized Triggers — Allows passing data with state transitions (e.g., SetVolume(int volume)); DynamicTriggerBehaviour implements generic trigger handlers for this feature
  • Fluent API / Builder Pattern — Stateless uses method chaining (Configure().Permit().OnEntry()) for readable, declarative state machine definitions instead of XML or tables
  • State Machine Introspection — StateGraph exports the entire machine topology for visualization (Mermaid, Graphviz Dot) and runtime inspection; enables debugging and documentation generation without hardcoding diagrams
  • Reentrant State Design — Stateless handles transitions back to the same state (reentrant) without re-executing OnEntry; prevents duplicate initialization and is essential for internal transitions like MuteMicrophone
  • NLiphardt/StateMachineEditor — Visual designer and editor for Stateless state machines; complements the library with tooling
  • Jiri-Mayer/Stateless.Graph — Extended graph export utilities for Stateless; adds additional diagram formats beyond core Mermaid/Dot support
  • akkadotnet/akka.net — Actor-based state machine and workflow alternative for distributed scenarios; Stateless is lighter for single-process state logic
  • Automatonymous/Automatonymous — Another C# state machine library with MassTransit integration; positioned for message-driven workflows vs. Stateless's direct API
  • dotnet-state-machine/tinstate — Sibling project from the same organization; ultra-lightweight state machine for simple use cases

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive unit tests for Graph module classes

The Graph module (src/Stateless/Graph/) contains visualization and state graph generation classes (MermaidGraph.cs, UmlDotGraph.cs, StateGraph.cs, Decision.cs, Transition.cs, SuperState.cs) but there are no corresponding test files visible in the repo. These classes are critical for diagram generation and need robust test coverage to prevent regressions in visualization output.

  • [ ] Create src/Stateless.Tests/Graph/ directory structure mirroring src/Stateless/Graph/
  • [ ] Add unit tests for MermaidGraph.cs covering different graph directions and styles
  • [ ] Add unit tests for UmlDotGraph.cs validating DOT format output
  • [ ] Add unit tests for StateGraph.cs, Decision.cs, and Transition.cs with various state machine configurations
  • [ ] Add integration tests validating graph output matches expected Mermaid/DOT syntax

Add unit tests for Reflection module StateInfo and StateMachineInfo classes

The Reflection module (src/Stateless/Reflection/) provides introspection capabilities for state machines through StateInfo.cs, StateMachineInfo.cs, DynamicTransitionInfo.cs, FixedTransitionInfo.cs, etc. These classes enable users to inspect state machine structure at runtime, but lack visible test coverage. This is a critical feature that needs validation.

  • [ ] Create src/Stateless.Tests/Reflection/ directory
  • [ ] Add unit tests for StateMachineInfo.cs validating state enumeration and transition discovery
  • [ ] Add unit tests for StateInfo.cs covering entry/exit actions and guard conditions
  • [ ] Add tests for FixedTransitionInfo.cs and DynamicTransitionInfo.cs with real state machines
  • [ ] Add tests for parameter conversion and invocation info reflection

Add comprehensive async/await tests for ReentryTriggerBehaviour and async variants

The codebase has split async implementations (ReentryTriggerBehaviour.async.cs, DynamicTriggerBehaviour.Async.cs, GuardConditionAsync.cs) alongside sync versions, but without visible corresponding test coverage. Given the complexity of async state machine behavior, this needs dedicated test scenarios covering concurrent transitions, cancellation, and async guard conditions.

  • [ ] Create or expand src/Stateless.Tests/ with async-specific test class
  • [ ] Add tests for async reentry behavior with Task-based entry/exit actions
  • [ ] Add tests for DynamicTriggerBehaviour.Async.cs with async state transitions
  • [ ] Add tests for GuardConditionAsync.cs validating guard condition async evaluation
  • [ ] Add concurrency tests validating state consistency during concurrent trigger invocations

🌿Good first issues

  • Add missing unit test coverage for DynamicTriggerBehaviour.Async.cs (async parameterized triggers); check if DynamicTriggerBehaviour.cs has parallel async tests
  • Document the SubstateOf() API with a code example in README.md showing how IsInState() behaves with hierarchical substates (currently only mentioned briefly)
  • Create a simple example project at example/OrderProcessingExample/ demonstrating guard clauses and parameterized triggers for e-commerce workflow (Order → Pending → Shipped with weight/size parameters)

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 588f1a1 — Merge pull request #644 from JMolenkamp/fix-include-async-triggers-in-state-info (mclift)
  • 974b848 — fix(state-machine-info): include async triggers when getting state info (JMolenkamp)
  • d0bfcfc — test(state-machine): add failing GetInfo test (JMolenkamp)
  • 523efd6 — Correction to CHANGELOG (mclift)
  • 7a19fb2 — #636 Include target SDK in build steps (mclift)
  • bc03633 — #636 add dotnet 10 build target (mclift)
  • de99c2f — Prepare release v5.20.0 (mclift)
  • 58afa5e — Merge pull request #631 from mclift/bugfix/629-async-superstate-transition-ignored (mclift)
  • 76e2a23 — Improve state names in tests (mclift)
  • 831e1ea — #629 Add tests to check multi-layer scenarios where a grandparent may be the closest ancestor with an open transition gu (mclift)

🔒Security observations

The Stateless state machine library demonstrates a strong security posture. As a utility library focused on state machine implementation, it has minimal external attack surface. No critical vulnerabilities were identified. The codebase appears to be well-maintained with a dedicated SECURITY.md file providing clear vulnerability reporting procedures via GitHub Security Advisories. The primary concern is the exposure of a Strong Name Key file in version control, which is a minor security hardening issue. No evidence of SQL injection, XSS, hardcoded credentials, insecure dependencies, or infrastructure misconfigurations was found based on the provided file structure and documentation.

  • Low · Strong Name Key File Exposed in Repository — asset/Stateless.snk. The file 'asset/Stateless.snk' (Strong Name Key file) is committed to the repository. While this is a public repository, it's a security best practice to keep cryptographic keys out of version control, even for non-sensitive development keys. Fix: Consider moving the SNK file to a secure location outside the repository or use Azure Key Vault / secure environment for key management. Update .gitignore to exclude sensitive key files.

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Mixed signals · dotnet-state-machine/stateless — RepoPilot