Agent skill
stateless
Guidance for Stateless state machine library for .NET. USE FOR: modeling state transitions with guards and actions, workflow engines, order processing pipelines, device lifecycle management, protocol implementations, approval workflows. DO NOT USE FOR: distributed state machines (use Durable Functions or Temporal), event sourcing (use Marten), full BPMN workflow engines (use Elsa), simple boolean flags.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/stateless
Metadata
Additional technical details for this skill
- author
- Tyler-R-Kendrick
- version
- 1.0.0
- displayName
- Stateless
SKILL.md
Stateless
Overview
Stateless is a lightweight library for creating state machines in .NET. It uses a fluent configuration API to define states, triggers, guard conditions, entry/exit actions, and sub-states. State machines built with Stateless are in-memory and synchronous by default, with support for async triggers.
Stateless is ideal for modeling domain workflows where an entity transitions through a defined set of states in response to events (triggers). Examples include order processing, device lifecycle, approval workflows, and protocol implementations.
Install via NuGet:
dotnet add package Stateless
Basic State Machine
Define states and triggers as enums, then configure valid transitions.
using Stateless;
public enum OrderState
{
Draft,
Submitted,
Approved,
Rejected,
Shipped,
Delivered,
Cancelled
}
public enum OrderTrigger
{
Submit,
Approve,
Reject,
Ship,
Deliver,
Cancel
}
var machine = new StateMachine<OrderState, OrderTrigger>(OrderState.Draft);
machine.Configure(OrderState.Draft)
.Permit(OrderTrigger.Submit, OrderState.Submitted)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled);
machine.Configure(OrderState.Submitted)
.Permit(OrderTrigger.Approve, OrderState.Approved)
.Permit(OrderTrigger.Reject, OrderState.Rejected)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled);
machine.Configure(OrderState.Approved)
.Permit(OrderTrigger.Ship, OrderState.Shipped)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled);
machine.Configure(OrderState.Shipped)
.Permit(OrderTrigger.Deliver, OrderState.Delivered);
// Fire triggers
Console.WriteLine($"State: {machine.State}"); // Draft
machine.Fire(OrderTrigger.Submit);
Console.WriteLine($"State: {machine.State}"); // Submitted
machine.Fire(OrderTrigger.Approve);
Console.WriteLine($"State: {machine.State}"); // Approved
Guard Conditions
Use guards to allow transitions only when specific conditions are met.
using Stateless;
public class OrderProcessor
{
private readonly StateMachine<OrderState, OrderTrigger> _machine;
private decimal _orderTotal;
private string? _approverName;
public OrderProcessor(decimal orderTotal)
{
_orderTotal = orderTotal;
_machine = new StateMachine<OrderState, OrderTrigger>(OrderState.Draft);
_machine.Configure(OrderState.Draft)
.PermitIf(OrderTrigger.Submit, OrderState.Submitted,
() => _orderTotal > 0, "Order total must be positive");
_machine.Configure(OrderState.Submitted)
.PermitIf(OrderTrigger.Approve, OrderState.Approved,
() => _approverName is not null, "Approver must be set")
.PermitIf(OrderTrigger.Approve, OrderState.Approved,
() => _orderTotal <= 10000, "Orders over $10,000 require VP approval")
.Permit(OrderTrigger.Reject, OrderState.Rejected);
_machine.Configure(OrderState.Approved)
.Permit(OrderTrigger.Ship, OrderState.Shipped);
}
public OrderState State => _machine.State;
public void SetApprover(string name) => _approverName = name;
public bool CanFire(OrderTrigger trigger) => _machine.CanFire(trigger);
public void Fire(OrderTrigger trigger) => _machine.Fire(trigger);
}
Entry and Exit Actions
Execute logic when entering or exiting a state.
using System;
using Stateless;
using Microsoft.Extensions.Logging;
public class DocumentWorkflow
{
private readonly StateMachine<DocState, DocTrigger> _machine;
private readonly ILogger _logger;
public enum DocState { Draft, Review, Approved, Published, Archived }
public enum DocTrigger { SubmitForReview, Approve, Reject, Publish, Archive }
public DocumentWorkflow(ILogger<DocumentWorkflow> logger)
{
_logger = logger;
_machine = new StateMachine<DocState, DocTrigger>(DocState.Draft);
_machine.Configure(DocState.Draft)
.Permit(DocTrigger.SubmitForReview, DocState.Review)
.OnExit(() => _logger.LogInformation("Document left draft state"));
_machine.Configure(DocState.Review)
.Permit(DocTrigger.Approve, DocState.Approved)
.Permit(DocTrigger.Reject, DocState.Draft)
.OnEntry(() =>
{
_logger.LogInformation("Document entered review at {Time}", DateTime.UtcNow);
NotifyReviewers();
})
.OnExit(transition =>
_logger.LogInformation("Leaving review via {Trigger}", transition.Trigger));
_machine.Configure(DocState.Approved)
.Permit(DocTrigger.Publish, DocState.Published)
.OnEntry(() => _logger.LogInformation("Document approved"));
_machine.Configure(DocState.Published)
.Permit(DocTrigger.Archive, DocState.Archived)
.OnEntry(() =>
{
_logger.LogInformation("Document published");
UpdateSearchIndex();
});
}
public DocState State => _machine.State;
public void Fire(DocTrigger trigger) => _machine.Fire(trigger);
private void NotifyReviewers() { /* send emails */ }
private void UpdateSearchIndex() { /* update search */ }
}
Parameterized Triggers
Pass data with triggers using parameterized trigger types.
using Stateless;
public class TicketMachine
{
public enum State { Open, Assigned, InProgress, Resolved, Closed }
public enum Trigger { Assign, StartWork, Resolve, Close, Reopen }
private readonly StateMachine<State, Trigger> _machine;
private readonly StateMachine<State, Trigger>.TriggerWithParameters<string> _assignTrigger;
private readonly StateMachine<State, Trigger>.TriggerWithParameters<string> _resolveTrigger;
private string? _assignee;
private string? _resolution;
public TicketMachine()
{
_machine = new StateMachine<State, Trigger>(State.Open);
_assignTrigger = _machine.SetTriggerParameters<string>(Trigger.Assign);
_resolveTrigger = _machine.SetTriggerParameters<string>(Trigger.Resolve);
_machine.Configure(State.Open)
.Permit(Trigger.Assign, State.Assigned);
_machine.Configure(State.Assigned)
.OnEntryFrom(_assignTrigger, assignee =>
{
_assignee = assignee;
Console.WriteLine($"Ticket assigned to {assignee}");
})
.Permit(Trigger.StartWork, State.InProgress)
.Permit(Trigger.Close, State.Closed);
_machine.Configure(State.InProgress)
.Permit(Trigger.Resolve, State.Resolved);
_machine.Configure(State.Resolved)
.OnEntryFrom(_resolveTrigger, resolution =>
{
_resolution = resolution;
Console.WriteLine($"Resolved: {resolution}");
})
.Permit(Trigger.Close, State.Closed)
.Permit(Trigger.Reopen, State.Open);
_machine.Configure(State.Closed)
.Permit(Trigger.Reopen, State.Open);
}
public void Assign(string assignee) => _machine.Fire(_assignTrigger, assignee);
public void Resolve(string resolution) => _machine.Fire(_resolveTrigger, resolution);
public void Fire(Trigger trigger) => _machine.Fire(trigger);
public State CurrentState => _machine.State;
}
Async State Machine
Use async entry/exit actions and async trigger firing.
using System.Threading.Tasks;
using Stateless;
public class AsyncPaymentProcessor
{
public enum State { Pending, Processing, Succeeded, Failed, Refunded }
public enum Trigger { Process, Succeed, Fail, Refund }
private readonly StateMachine<State, Trigger> _machine;
public AsyncPaymentProcessor()
{
_machine = new StateMachine<State, Trigger>(State.Pending);
_machine.Configure(State.Pending)
.Permit(Trigger.Process, State.Processing);
_machine.Configure(State.Processing)
.OnEntryAsync(async () =>
{
await CallPaymentGatewayAsync();
})
.Permit(Trigger.Succeed, State.Succeeded)
.Permit(Trigger.Fail, State.Failed);
_machine.Configure(State.Succeeded)
.OnEntryAsync(async () =>
{
await SendConfirmationEmailAsync();
})
.Permit(Trigger.Refund, State.Refunded);
_machine.Configure(State.Failed)
.Permit(Trigger.Process, State.Processing); // retry
}
public async Task ProcessAsync()
{
await _machine.FireAsync(Trigger.Process);
}
public State CurrentState => _machine.State;
private async Task CallPaymentGatewayAsync() => await Task.Delay(100);
private async Task SendConfirmationEmailAsync() => await Task.Delay(50);
}
Generating State Machine Diagrams
Stateless can export its configuration to DOT format for visualization.
using Stateless;
using Stateless.Graph;
var machine = new StateMachine<OrderState, OrderTrigger>(OrderState.Draft);
// ... configure machine ...
// Generate DOT graph
string dotGraph = UmlDotGraph.Format(machine.GetInfo());
Console.WriteLine(dotGraph);
// Output can be rendered with Graphviz or online tools
// Query permitted triggers
var permitted = machine.GetPermittedTriggers();
Console.WriteLine($"Permitted triggers: {string.Join(", ", permitted)}");
Best Practices
- Define states and triggers as enums rather than strings to get compile-time safety and prevent typos in state/trigger names.
- Use guard conditions (
PermitIf) to enforce business rules at the transition level rather than checking preconditions in calling code. - Provide human-readable guard descriptions as the last parameter to
PermitIfso thatGetPermittedTriggersand diagram exports show why a transition is blocked. - Use
OnEntry/OnExitactions for side effects (logging, notifications, database updates) rather than placing them in the code that callsFire. - Call
CanFirebeforeFirein UI-driven scenarios to enable/disable buttons based on valid transitions, preventingInvalidOperationException. - Use parameterized triggers to pass context data (assignee, reason, amount) into entry actions rather than setting instance fields before firing.
- Use
FireAsyncandOnEntryAsyncwhen entry/exit actions involve I/O operations (database, HTTP calls) to avoid blocking threads. - Externalize state storage by using the
StateMachine<TState, TTrigger>(stateAccessor, stateMutator)constructor overload to persist state in a database or cache. - Export DOT diagrams with
UmlDotGraph.Formatduring development to visually verify that the state machine matches the intended workflow design. - Handle
InvalidOperationExceptionfrom invalid transitions gracefully by logging the current state and attempted trigger rather than letting the exception propagate unhandled.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?