# AshStateMachine AshStateMachine is an Elixir extension for the Ash Framework that enables building robust finite state machines directly within Ash resources. It provides a declarative DSL for defining states, transitions, and validation rules, transforming standard Ash resources into state-aware entities that enforce strict transition logic at the framework level. Unlike traditional state machine implementations that rely on external workflow engines, AshStateMachine leverages Ash's action system to provide compile-time validation and runtime enforcement of state transitions. The library seamlessly integrates with the entire Ash ecosystem, including policies, authorization, change tracking, and data persistence layers. It automatically generates state attributes, validates transition paths, and provides utilities for introspection and visualization. By treating state machines as resource extensions rather than separate abstractions, AshStateMachine eliminates the complexity of coordinating between business logic and state management while maintaining the flexibility to handle complex workflows with conditional transitions, wildcard patterns, and deprecated states. ## Defining a Basic State Machine Resource Define a resource with state machine capabilities by adding the AshStateMachine extension and declaring initial states and transitions. ```elixir defmodule Helpdesk.Support.Ticket do use Ash.Resource, domain: Helpdesk.Support, data_layer: Ash.DataLayer.Ets, extensions: [AshStateMachine] state_machine do initial_states [:received] default_initial_state :received transitions do transition :request_more_information, from: [:received, :with_it, :with_hr], to: :needs_more_info transition :assign_to_department, from: [:received, :needs_more_info], to: [:with_it, :with_hr] transition :close, from: [:with_it, :with_hr, :needs_more_info], to: :closed end end attributes do uuid_primary_key :id attribute :subject, :string attribute :description, :string # :state attribute is automatically added end actions do defaults [:read, :create] update :request_more_information do change transition_state(:needs_more_info) end update :close do change transition_state(:closed) end end code_interface do define :create define :request_more_information define :close end end # Usage {:ok, ticket} = Helpdesk.Support.Ticket.create(%{subject: "Login issue"}) # ticket.state => :received {:ok, ticket} = Helpdesk.Support.Ticket.request_more_information(ticket) # ticket.state => :needs_more_info {:ok, ticket} = Helpdesk.Support.Ticket.close(ticket) # ticket.state => :closed ``` ## Conditional State Transitions with Arguments Use action arguments to conditionally transition to different states based on runtime input. ```elixir defmodule Helpdesk.Support.Ticket do use Ash.Resource, domain: Helpdesk.Support, extensions: [AshStateMachine] state_machine do initial_states [:received] default_initial_state :received transitions do transition :assign_to_department, from: [:received, :needs_more_info], to: [:with_it, :with_hr] end end actions do update :assign_to_department do argument :department, :atom, allow_nil?: false, constraints: [one_of: [:IT, :HR]] change fn changeset, _context -> department = Ash.Changeset.get_argument(changeset, :department) target_state = if department == :IT, do: :with_it, else: :with_hr AshStateMachine.transition_state(changeset, target_state) end end end code_interface do define :assign_to_department end end # Usage {:ok, ticket} = Helpdesk.Support.Ticket.create(%{subject: "Password reset"}) {:ok, ticket} = Helpdesk.Support.Ticket.assign_to_department(ticket, %{department: :IT}) # ticket.state => :with_it {:ok, ticket2} = Helpdesk.Support.Ticket.create(%{subject: "Benefits question"}) {:ok, ticket2} = Helpdesk.Support.Ticket.assign_to_department(ticket2, %{department: :HR}) # ticket2.state => :with_hr ``` ## Wildcard Transitions for Universal State Changes Use `:*` wildcards to allow transitions from any state or to multiple states, useful for error handling and cancellation flows. ```elixir defmodule Order do use Ash.Resource, domain: Shop, extensions: [AshStateMachine] state_machine do initial_states [:pending] default_initial_state :pending transitions do transition :confirm, from: :pending, to: :confirmed transition :begin_delivery, from: :confirmed, to: :on_its_way transition :package_arrived, from: :on_its_way, to: :arrived transition :error, from: [:pending, :confirmed, :on_its_way], to: :error # Allow abort from any state transition :abort, from: :*, to: :aborted end end actions do defaults [:create, :read] update :confirm do change transition_state(:confirmed) end update :begin_delivery do change transition_state(:on_its_way) end update :package_arrived do change transition_state(:arrived) end update :error do accept [:error_message] change transition_state(:error) end update :abort do change transition_state(:aborted) end end attributes do uuid_primary_key :id attribute :error_message, :string end code_interface do define :create define :confirm define :begin_delivery define :abort end end # Usage - abort from any state {:ok, order1} = Order.create(%{}) {:ok, order1} = Order.abort(order1) # order1.state => :aborted (from :pending) {:ok, order2} = Order.create(%{}) {:ok, order2} = Order.confirm(order2) {:ok, order2} = Order.begin_delivery(order2) {:ok, order2} = Order.abort(order2) # order2.state => :aborted (from :on_its_way) ``` ## Automatic Next State Transition Use `next_state/0` to automatically transition when only one valid next state exists. ```elixir defmodule Workflow do use Ash.Resource, domain: ProcessManagement, extensions: [AshStateMachine] state_machine do initial_states [:a] default_initial_state :a transitions do transition :next, from: :a, to: :b transition :next, from: :b, to: [:c, :d] transition :next, from: :c, to: :e end end actions do defaults [:create] update :next do change next_state() end end code_interface do define :create define :next end end # Usage {:ok, wf} = Workflow.create(%{state: :a}) {:ok, wf} = Workflow.next(wf) # wf.state => :b (only one valid next state from :a) {:error, reason} = Workflow.next(wf) # Error: multiple next states available [:c, :d] {:ok, wf} = Workflow.create(%{state: :c}) {:ok, wf} = Workflow.next(wf) # wf.state => :e ``` ## Custom State Attribute Name Override the default `:state` attribute name with a custom attribute. ```elixir defmodule Document do use Ash.Resource, domain: CMS, extensions: [AshStateMachine] state_machine do initial_states [:draft] default_initial_state :draft state_attribute :status # Use :status instead of :state transitions do transition :submit, from: :draft, to: :review transition :approve, from: :review, to: :published transition :reject, from: :review, to: :draft end end attributes do uuid_primary_key :id attribute :title, :string attribute :content, :string # :status attribute automatically added instead of :state end actions do defaults [:create] update :submit do change transition_state(:review) end update :approve do change transition_state(:published) end end code_interface do define :create define :submit define :approve end end # Usage {:ok, doc} = Document.create(%{title: "Article", content: "..."}) # doc.status => :draft (note: status, not state) {:ok, doc} = Document.submit(doc) # doc.status => :review ``` ## Querying Possible Next States Use introspection functions to determine valid transitions for a record. ```elixir defmodule ThreeStates do use Ash.Resource, domain: Domain, extensions: [AshStateMachine] state_machine do initial_states [:pending] default_initial_state :pending transitions do transition :begin, from: :pending, to: :executing transition :complete, from: :executing, to: :complete transition :reset, from: [:executing, :complete], to: :pending end end actions do defaults [:create] update :begin do change transition_state(:executing) end update :complete do change transition_state(:complete) end update :reset do change transition_state(:pending) end end code_interface do define :create define :begin end end # Usage - all possible next states regardless of action {:ok, record} = ThreeStates.create(%{}) states = AshStateMachine.possible_next_states(record) # => [:executing] {:ok, record} = ThreeStates.begin(record) states = AshStateMachine.possible_next_states(record) # => [:complete, :pending] # Usage - next states for specific action states_for_reset = AshStateMachine.possible_next_states(record, :reset) # => [:pending] ``` ## Generating State Machine Visualizations Generate Mermaid diagrams for documentation and visualization of state transitions. ```elixir defmodule Order do use Ash.Resource, domain: Shop, extensions: [AshStateMachine] state_machine do initial_states [:pending] default_initial_state :pending transitions do transition :confirm, from: :pending, to: :confirmed transition :ship, from: :confirmed, to: :shipped transition :deliver, from: :shipped, to: :delivered transition :cancel, from: [:pending, :confirmed], to: :cancelled end end end # Generate flowchart flowchart = AshStateMachine.Charts.mermaid_flowchart(Order) # => """ # flowchart TD # pending --> |confirm| confirmed # pending --> |cancel| cancelled # confirmed --> |ship| shipped # confirmed --> |cancel| cancelled # shipped --> |deliver| delivered # """ # Generate state diagram state_diagram = AshStateMachine.Charts.mermaid_state_diagram(Order) # => """ # stateDiagram-v2 # pending --> confirmed: confirm # pending --> cancelled: cancel # confirmed --> shipped: ship # confirmed --> cancelled: cancel # shipped --> delivered: deliver # """ # Use in documentation or render with Mermaid.js IO.puts(flowchart) ``` ## Policy-Based Transition Authorization Integrate with Ash policies to control who can perform state transitions. ```elixir defmodule Order do use Ash.Resource, domain: Shop, extensions: [AshStateMachine], authorizers: [Ash.Policy.Authorizer] state_machine do initial_states [:pending] default_initial_state :pending transitions do transition :confirm, from: :pending, to: :confirmed transition :ship, from: :confirmed, to: :shipped transition :deliver, from: :shipped, to: :delivered end end policies do # Only allow valid state transitions policy always() do authorize_if AshStateMachine.Checks.ValidNextState end # Additional business rules policy action(:ship) do authorize_if actor_attribute_equals(:role, :warehouse_staff) end policy action(:deliver) do authorize_if actor_attribute_equals(:role, :delivery_driver) end end actions do defaults [:create, :read] update :confirm do change transition_state(:confirmed) end update :ship do change transition_state(:shipped) end update :deliver do change transition_state(:delivered) end end end # Usage with actor {:ok, order} = Order.create(%{}) # Customer can confirm {:ok, order} = Order.confirm(order, actor: %User{role: :customer}) # Only warehouse staff can ship {:ok, order} = Order.ship(order, actor: %User{role: :warehouse_staff}) # {:error, _} = Order.ship(order, actor: %User{role: :customer}) ``` ## Introspecting State Machine Configuration Use the Info module to query state machine configuration at runtime. ```elixir defmodule Ticket do use Ash.Resource, domain: Support, extensions: [AshStateMachine] state_machine do initial_states [:new] default_initial_state :new state_attribute :status extra_states [:archived] deprecated_states [:legacy_state] transitions do transition :assign, from: :new, to: :assigned transition :resolve, from: :assigned, to: :resolved end end end # Query initial states AshStateMachine.Info.state_machine_initial_states!(Ticket) # => [:new] # Get default initial state AshStateMachine.Info.state_machine_default_initial_state!(Ticket) # => :new # Get state attribute name AshStateMachine.Info.state_machine_state_attribute!(Ticket) # => :status # Get all possible states AshStateMachine.Info.state_machine_all_states(Ticket) # => [:new, :assigned, :resolved, :archived] # Get transitions for specific action AshStateMachine.Info.state_machine_transitions(Ticket, :assign) # => [%AshStateMachine.Transition{action: :assign, from: [:new], to: [:assigned]}] # Get all transitions AshStateMachine.Info.state_machine_transitions(Ticket) # => [%AshStateMachine.Transition{...}, ...] ``` ## Error Handling with State Transitions Implement automatic error state transitions and recovery workflows. ```elixir defmodule Order do use Ash.Resource, domain: Shop, extensions: [AshStateMachine] state_machine do initial_states [:pending] default_initial_state :pending transitions do transition :confirm, from: :pending, to: :confirmed transition :ship, from: :confirmed, to: :shipped transition :error, from: [:pending, :confirmed, :shipped], to: :error transition :retry, from: :error, to: :pending end end actions do defaults [:create] update :confirm do change transition_state(:confirmed) end update :ship do change transition_state(:shipped) end update :error do accept [:error_state, :error_message] change transition_state(:error) end update :retry do change transition_state(:pending) end end # Automatically catch errors and transition to error state changes do change after_transaction(fn _changeset, {:ok, result}, _ -> {:ok, result} changeset, {:error, error}, _ -> if changeset.context[:error_handler?] do {:error, error} else changeset.data |> Ash.Changeset.for_update(:error, %{ error_state: changeset.data.state, error_message: Exception.message(error) }) |> Ash.Changeset.set_context(%{error_handler?: true}) |> Ash.update() {:error, error} end end), on: [:update] end attributes do uuid_primary_key :id attribute :error_state, :string attribute :error_message, :string end code_interface do define :create define :confirm define :retry end end # Usage {:ok, order} = Order.create(%{}) {:ok, order} = Order.confirm(order) # If an error occurs, automatically transitions to :error state # order.state => :error # order.error_state => "confirmed" # order.error_message => "..." # Retry from error state {:ok, order} = Order.retry(order) # order.state => :pending ``` ## AshStateMachine provides a declarative, type-safe approach to modeling complex workflows within Ash resources by treating state machines as first-class resource extensions. The primary use cases include order processing systems with multiple fulfillment stages, ticket management systems with triage and assignment workflows, document approval processes with review cycles, and any domain requiring strict state transition validation. The library excels at preventing invalid state transitions at compile-time, automatically generating state attributes with proper constraints, and seamlessly integrating with Ash's policy system for fine-grained authorization controls. ## Integration patterns center around extending existing Ash resources with the `extensions: [AshStateMachine]` declaration, then defining states and transitions in the `state_machine` block while using the `transition_state/1` and `next_state/0` changes within action definitions. The framework handles all validation automatically, ensuring transitions respect the defined rules without requiring manual state checking. For visualization and introspection, the `AshStateMachine.Charts` module generates Mermaid diagrams, while `AshStateMachine.Info` provides runtime queries for state machine configuration. Complex scenarios like conditional transitions, error recovery workflows, and policy-based authorization integrate naturally through Ash's standard change, validation, and policy mechanisms, making state machines feel like native resource capabilities rather than bolted-on abstractions.