documentation/topics/advanced/multi-step-actions.md

# Complex & Multi-Step Actions

Multi-step actions in Ash allow you to create sophisticated workflows that coordinate multiple changes or processes together. Often business logic crosses multiple resources, and we often want it to be transcational. By leveraging action lifecycle hooks, you can build powerful domain-specific operations. This guide will explore how to build and use multi-step actions using a helpdesk example. In some cases, hooks can grow to a point where they are unwieldy. You should use hooks for small-to-medium complexity workflows for simplicity's sake. If you need more, look into [reactor](/documentation/topics/advanced/reactor.md), which is the "big kahuna" of orchestrating complex multi-step workflows.

> ### Durable Workflows {: .info}
>
> For durabile workflows, we suggest to use Oban. We provide tools to integrate with oban in [AshOban](hexdocs.pm/ash_oban). AshOban supports very specific types of common workflows, like "triggers" that run periodically for resources, and "scheduled actions" which run generic actions on a cron. You should not be afraid to write "standard" oban jobs and code where possible. Don't bend over backwards trying to fit everything into AshOban.

## Action Lifecycle Hooks

At the core of Ash's multi-step action capability are action lifecycle hooks. These hooks allow you to run code at specific points during an action's execution:

- **before_transaction**: Runs before the transaction is started. Useful for operations that should happen before the transaction, like external API calls.

- **before_action**: Runs in the transaction, before the data layer is called. Perfect for side effects and expensive logic. This hook can be used with changesets and queries.

- **after_action**: Runs in the transaction, after the data layer is called, only if the action is successful. Ideal for transactional side effects that should only happen on success. This hook can be used with changesets and queries.

- **after_transaction**: Runs after the transaction completes, in both success and error cases. Ideal for operations that should happen regardless of the transaction outcome, and for operations that work with external services.

There are other hooks that we won't go into here, as they are rarely used. See the documentation in `Ash.Changeset` for more.

- **around_action**: Runs code both before and after the action logic, within the transaction.
- **around_transaction**: Runs code both before and after the transaction, outside the transaction.

### Creating a Custom Change with Hooks

Let's see how to implement hooks using a custom change module. Here's an example for a helpdesk ticket system:

```elixir
defmodule HelpDesk.Changes.AssignTicket do
  use Ash.Resource.Change

  @impl true
  def init(opts) do
    # Validate the options
    # This is run at compile time
    {:ok, opts}
  end

  @impl true
  def change(changeset, opts, _context) do
    # Add a before_action hook to perform the assignment logic
    Ash.Changeset.before_action(changeset, fn changeset ->
      # Find an available agent
      case HelpDesk.AgentManager.find_available_agent() do
        {:ok, agent} ->
          # Assign the agent to the ticket
          changeset
          |> Ash.Changeset.change_attribute(:agent_id, agent.id)
          |> Ash.Changeset.change_attribute(:status, "assigned")
          |> Ash.Changeset.put_context(:assigned_agent, agent)

        {:error, reason} ->
          # Add an error if no agent is available
          Ash.Changeset.add_error(changeset, message: "No agents available: #{reason}")
      end
    end)
    |> Ash.Changeset.after_action(fn changeset, result ->
      # Notify the agent after successful assignment
      if agent = Ash.Changeset.get_context(changeset, :assigned_agent) do
        HelpDesk.Notifications.notify_agent(agent, result)
      end

      # Return the result
      {:ok, result}
    end)
  end
end
```

You would use this change in your Ticket resource like this:

```elixir
actions do
  create :open_and_assign do
    accept [:title, :description, :priority]

    # Apply our custom change that adds the hooks
    change {HelpDesk.Changes.AssignTicket, []}
  end
end
```

### Shortcuts for hooks

There are multiple ways to add hooks to actions. What we showed above, defining an `Ash.Resource.Change` module, and using the functions in `Ash.Changeset` to add callbacks to the module is the most organized & idiomatic way. However, you can also use the following techniques as shorthand. In general, prefer to write your changes into modules to keep your resources clean and keep compile times down (more smaller modules is typically better than fewer larger modules).

#### Anonymous Function Changes

Here we use an anonymous function change, which is a shorthand for defining a change module with a single function. Just like in the `change/3` function, you can add hooks to the changeset.

```elixir
create :open_and_assign do
  change fn changeset, context ->
    Ash.Changeset.after_action(changeset, fn changeset, result ->
      ...
    end)
  end
end
```

#### Builtin Hook Changes

If you know that you just want to add a single hook to an action, you can use some of the functions in `Ash.Resource.Change.Builtins`, which are simple shorhands for the above form. For example:

```elixir
create :open_and_assign do
  change after_action(changeset, result, context ->
    ...
  end)
end
```

Notice how the anonymous function takes an extra argument. In the first format above, the `context` value came from `change fn changeset, context ->`, but in this format, it is provided as an argument at the end of the builtin function, i.e `change after_action(changeset, result, context -> .`