View Source Getting Started with State Machines

Get familiar with Ash resources

If you haven't already, read the Ash Getting Started Guide, and familiarize yourself with Ash and Ash resources.

Bring in the ash_state_machine dependency

def deps()
  [
    ...
    {:ash_state_machine, "~> 0.2.2"}
  ]
end

Making a resource into a state machine

The concept of a state machine (in this case a "Finite State Machine"), essentially involves a single state, with specified transitions between states. For example, you might have an order state machine with states [:pending, :on_its_way, :delivered]. However, you can't go from :pending to :delivered (probably), and so you want to only allow certain transitions in certain circumstances, i.e :pending -> :on_its_way -> :delivered.

This extension's goal is to help you write clear and clean state machines, with all of the extensibility and power of Ash resources and actions.

A basic state machine

defmodule Order do
  # leaving out data layer configuration for brevity
  use Ash.Resource,
    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
    end
  end

  actions do
    # create sets the state
    defaults [:create, :read]

    update :confirm do
      # accept [...]
      # you can change other attributes
      # or do anything else an action can normally do
      # this transition will be validated according to
      # the state machine rules above
      change transition_state(:confirmed)
    end

    update :begin_delivery do
      # accept [...]
      change transition_state(:on_its_way)
    end

    update :package_arrived do
      # accept [...]
      change transition_state(:arrived)
    end

    update :error do
      accept [:error_state, :error]
      change transition_state(:error)
    end
  end

  changes do
    # any failures should be captured and transitioned to the error state
    change after_transaction(fn
              changeset, {:ok, result} ->
                {:ok, result}

              changeset, {:error, error} ->
                message = Exception.message(error)

                changeset.data
                |> Ash.Changeset.for_update(:error, %{
                  message: message,
                  error_state: changeset.data.state
                })
                |> Api.update()
            end),
            on: [:update]
  end

  attributes do
    uuid_primary_key :id
    # ...attributes like address/delivery options would go here
    attribute :error, :string
    attribute :error_state, :string
    # :state attribute is added for you by `state_machine`
    # however, you can add it yourself, and you will be guided by
    # compile errors on what states need to be allowed by your type.
  end
end

Generating Flow Charts

run mix ash_state_machine.generate_flow_charts to generate flow charts for your resources. See the task documentation for more. Here is a chart generated from the example above:

stateDiagram-v2
pending --> confirmed: confirm
confirmed --> on_its_way: begin_delivery
on_its_way --> arrived: package_arrived
on_its_way --> error: error
confirmed --> error: error
pending --> error: error

Learning more