Resource

Ash resources are used to model data and define actions which are used to manipulate that data. In the Ash world we often compare the resources with nouns and the actions with verbs.

In Ash 3.0, resources are grouped into domains - context boundaries where related resources are defined together. This helps organize your application and makes it easier to understand the relationships between resources.

To-Do-List Example

To dive into resources we use a simple to-do-list application as an example. As a preparation for this, use Igniter to create a new application:

$ mix archive.install hex igniter_new
$ mix igniter.new app --install ash
$ cd app

Alternatively, you can follow the Ash Setup Guide for other setup options.

We want to create a task resource which has a content attribute and an id attribute as a primary key. We also want to include the actions create, read, update and delete. Ash provides those actions for free but we have to include them into the resource.

In Ash 3.x, resources live inside a domain. The domain is a context boundary for a set of related resources and a place to hang shared functionality (code interfaces, policies, pub/sub).

Configure the Domain

First, we register our domain in config/config.exs so Ash knows about it at compile time (otherwise Ash prints a warning on boot):

config/config.exs
import Config

config :app, :ash_domains, [App.ToDoList]

Now we create the ToDoList domain module which contains the resource Task.

lib/app/to_do_list.ex
defmodule App.ToDoList do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.ToDoList.Task
  end
end

For the resource(s) we create a new directory:

$ mkdir -p lib/app/to_do_list

Configure the Resource

The resource defines attributes which are the fields of the resource. In our case we have two attributes: id and content. The id attribute is special: it is the primary key of the resource, and the uuid_primary_key macro handles type, default, and indexing for us. The content attribute is a plain string.

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id
    attribute :content, :string, public?: true (1)
  end
end
1 Ash 3.x attributes are private by default. Mark :content as public?: true so that actions can accept it as input and so that JSON/GraphQL layers can expose it. Forgetting this flag is the single most common Ash beginner trap, the error message is helpful but nothing prints the first time you try it.

In this example we use the Ash.DataLayer.Ets as a database layer. ETS (Erlang Term Storage) is an in-memory data store which is built into your Erlang system. For our training purpose this is ideal because we don’t have to install and configure a database.

ETS does not save any data to disk! With every restart of iex you have to re-create the example data. For production applications, you should use AshPostgres or another persistent data layer.

The resulting directory structure should look like this:

$ tree lib
lib
├── app
│   ├── to_do_list
│   │   └── task.ex
│   └── to_do_list.ex
└── app.ex

3 directories, 3 files

We now have a resource but because we haven’t defined any actions we can’t do anything with it yet. Let’s change that.

Create

To create a resource, we need to add the create action to the resource. In Ash 3.0, we also add a code interface to our domain: a short define :name, action: :some_action line that generates a plain Elixir function (for example App.ToDoList.create_task/1) so you can call the action directly, instead of building a changeset by hand.

First, let’s add the create action to our resource:

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id
    attribute :content, :string, public?: true (1)
  end

  actions do
    default_accept [:content] (2)
    defaults [:create]
  end
end
1 Mark the attribute as public so that the create action can accept it. In Ash 3.x, attributes are private by default.
2 default_accept [:content] declares which attributes the default actions accept as input. Without it, create would reject the content field.

Then, we add a code interface definition to our domain:

lib/app/to_do_list.ex
defmodule App.ToDoList do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.ToDoList.Task do
      define :create_task, action: :create
    end
  end
end

This creates a App.ToDoList.create_task/1-2 function that we can use to create tasks.

Fire up the IEx (Elixir’s Interactive Shell) to create your first task:

$ iex -S mix
Compiling 2 files (.ex)
Erlang/OTP 28 [erts-16.2.2] [...]

Interactive Elixir (1.20.0-rc.4) [...]
iex(1)> App.ToDoList.create_task!(%{content: "Mow the lawn"})
%App.ToDoList.Task{
  id: "8e868c09-c0d0-4362-8270-09272acab769",
  content: "Mow the lawn",
  __meta__: #Ecto.Schema.Metadata<:loaded>
}
iex(2)>
Ash 3.x logs a [debug] Creating App.ToDoList.Task: Setting %{…​} line before each write by default. It is harmless and the book omits it to keep the output compact. If you want to suppress it yourself, call Logger.configure(level: :info) at the top of your IEx session, or add config :logger, level: :info to config/dev.exs.

The function App.ToDoList.create_task!/1-2 raises an error if something goes wrong (e.g. a validation error). Alternatively you can use App.ToDoList.create_task/1-2 which returns a tuple with the status and the resource.

iex(2)> App.ToDoList.create_task(%{content: "Mow the lawn"})
{:ok,
 %App.ToDoList.Task{
   id: "a8430505-ef7e-4f64-bc2c-2a6db216d8ea",
   content: "Mow the lawn",
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(3)>

You can still create a task the long way with the following code:

App.ToDoList.Task
|> Ash.Changeset.for_create(:create, %{content: "Mow the lawn"})
|> Ash.create!()

The create_task/1-2 code interface function is just a lot more convenient.

Read

Writing is one thing, but it only pays off if we can read the data back. Let us add a read action to the resource, and matching code interfaces on the domain.

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id
    attribute :content, :string, public?: true
  end

  actions do
    default_accept [:content]
    defaults [:create, :read] (1)
  end
end
1 Add :read to the list of default actions.
lib/app/to_do_list.ex
defmodule App.ToDoList do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.ToDoList.Task do
      define :create_task, action: :create
      define :list_tasks, action: :read (1)
      define :get_task, action: :read, get_by: [:id] (2)
    end
  end
end
1 list_tasks returns every task.
2 get_task is a read action that looks up a single task by its primary key. The get_by: [:id] option tells Ash to expose a bang and non-bang function that takes the id directly.

Index: list all tasks

list_tasks/0 (and its raising sibling list_tasks!/0) returns every task in the store:

$ iex -S mix
Erlang/OTP 28 [erts-16.2.2] [...]

Interactive Elixir (1.20.0-rc.4) [...]
iex(1)> App.ToDoList.create_task!(%{content: "Mow the lawn"})
%App.ToDoList.Task{
  id: "881c6c08-223c-41b1-9d61-2d3a40e478bd",
  content: "Mow the lawn",
  __meta__: #Ecto.Schema.Metadata<:loaded>
}
iex(2)> App.ToDoList.create_task!(%{content: "Buy milk"})
%App.ToDoList.Task{
  id: "22b11587-20fe-40d2-830e-50f8930c13c9",
  content: "Buy milk",
  __meta__: #Ecto.Schema.Metadata<:loaded>
}
iex(3)> App.ToDoList.list_tasks!() |> Enum.map(& &1.content)
["Buy milk", "Mow the lawn"]
iex(4)> App.ToDoList.list_tasks()
{:ok,
 [
   %App.ToDoList.Task{
     id: "881c6c08-223c-41b1-9d61-2d3a40e478bd",
     content: "Mow the lawn",
     __meta__: #Ecto.Schema.Metadata<:loaded>
   },
   %App.ToDoList.Task{
     id: "22b11587-20fe-40d2-830e-50f8930c13c9",
     content: "Buy milk",
     __meta__: #Ecto.Schema.Metadata<:loaded>
   }
 ]}
ETS does not guarantee row order, so the two tasks can come back in either order. In production, use sort in a read action (or Enum.sort_by/2) when ordering matters.

An empty store simply returns an empty list:

iex(1)> App.ToDoList.list_tasks!()
[]
iex(2)> App.ToDoList.list_tasks()
{:ok, []}

Show: fetch a single task by id

get_task/1 and get_task!/1 look up a task by its primary key:

iex(1)> {:ok, task} = App.ToDoList.create_task(%{content: "Mow the lawn"})
{:ok, %App.ToDoList.Task{id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
  content: "Mow the lawn", __meta__: #Ecto.Schema.Metadata<:loaded>}}
iex(2)> App.ToDoList.get_task(task.id)
{:ok,
 %App.ToDoList.Task{
   id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
   content: "Mow the lawn",
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(3)> App.ToDoList.get_task!(task.id)
%App.ToDoList.Task{
  id: "a5648b48-4eb3-443d-aba7-fafbbfedc564",
  content: "Mow the lawn",
  __meta__: #Ecto.Schema.Metadata<:loaded>
}

When the id does not exist, the non-bang version returns an error tuple and the bang version raises:

iex(1)> App.ToDoList.get_task("not-in-the-db")
{:error,
 %Ash.Error.Invalid{
   errors: [
     %Ash.Error.Query.NotFound{
       primary_key: nil,
       resource: App.ToDoList.Task,
       ...
     }
   ]
 }}
iex(2)> App.ToDoList.get_task!("not-in-the-db")
** (Ash.Error.Invalid) Invalid Error
[...]
* record not found
    [...]
Ash 3.x wraps errors in Ash.Error.Invalid and exposes the individual errors in the errors: list. The bang version raises Ash.Error.Invalid rather than the inner error class; pattern match on the inner errors if you need to branch on the cause. # Update

By now the pattern should feel familiar: add an action to the resource, expose a code interface on the domain, call the generated function.

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id
    attribute :content, :string, public?: true
  end

  actions do
    default_accept [:content]
    defaults [:create, :read, :update] (1)
  end
end
1 Added :update to the defaults.
lib/app/to_do_list.ex
defmodule App.ToDoList do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.ToDoList.Task do
      define :create_task, action: :create
      define :list_tasks, action: :read
      define :get_task, action: :read, get_by: [:id]
      define :update_task, action: :update (1)
    end
  end
end
1 Exposes update_task/2 (and update_task!/2).

Let’s try it in IEx:

$ iex -S mix
Erlang/OTP 28 [erts-16.2.2] [...]

Interactive Elixir (1.20.0-rc.4) [...]
iex(1)> {:ok, task} = App.ToDoList.create_task(%{content: "Mow the lawn"})
{:ok,
 %App.ToDoList.Task{
   id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
   content: "Mow the lawn",
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(2)> App.ToDoList.update_task(task, %{content: "Play golf"})
{:ok,
 %App.ToDoList.Task{
   id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
   content: "Play golf",
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(3)> App.ToDoList.update_task!(task, %{content: "Buy milk"})
%App.ToDoList.Task{
  id: "d4c8cb9a-10b7-45f4-bece-dcea0fd16e5f",
  content: "Buy milk",
  __meta__: #Ecto.Schema.Metadata<:loaded>
}

The first argument is the record you are updating (so Ash knows which row to change), the second is the map of new values. # Destroy (delete)

Last of the four CRUD actions is :destroy. Same pattern as the others:

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id
    attribute :content, :string, public?: true
  end

  actions do
    default_accept [:content]
    defaults [:create, :read, :update, :destroy] (1)
  end
end
1 Added :destroy to the defaults.
lib/app/to_do_list.ex
defmodule App.ToDoList do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.ToDoList.Task do
      define :create_task, action: :create
      define :list_tasks, action: :read
      define :get_task, action: :read, get_by: [:id]
      define :update_task, action: :update
      define :destroy_task, action: :destroy (1)
    end
  end
end
1 Exposes destroy_task/1 and destroy_task!/1.

Let’s try it out:

$ iex -S mix
Erlang/OTP 28 [erts-16.2.2] [...]

Interactive Elixir (1.20.0-rc.4) [...]
iex(1)> {:ok, task} = App.ToDoList.create_task(%{content: "Mow the lawn"})
{:ok,
 %App.ToDoList.Task{
   id: "5bd2b15e-fd29-4d3f-9356-cbfe06ea7eee",
   content: "Mow the lawn",
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(2)> App.ToDoList.destroy_task(task)
:ok
iex(3)> App.ToDoList.get_task(task.id) (1)
{:error,
 %Ash.Error.Invalid{
   errors: [
     %Ash.Error.Query.NotFound{
       primary_key: nil,
       resource: App.ToDoList.Task,
       ...
     }
   ]
 }}
iex(4)>
1 The task has been destroyed, so a get_task lookup by the old id returns an error tuple. See read for details on Ash 3.x’s Ash.Error.Invalid wrapper.

Defaults

Attributes can declare a default value, plus a few constraints that keep bad data out. Let us add two new attributes to our task: a priority integer (optional, 1-3) and an is_done boolean (required, defaults to false).

lib/app/to_do_list/task.ex
defmodule App.ToDoList.Task do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets,
    domain: App.ToDoList,
    otp_app: :app

  attributes do
    uuid_primary_key :id

    attribute :content, :string do
      allow_nil? false (1)
      public? true
      constraints min_length: 1, max_length: 255
    end

    attribute :priority, :integer do
      allow_nil? true
      public? true
      constraints min: 1, max: 3
    end

    attribute :is_done, :boolean do
      allow_nil? false
      public? true
      default false (2)
    end
  end

  actions do
    default_accept [:content, :priority, :is_done]
    defaults [:create]
  end
end
1 allow_nil? false makes the database reject nil and the changeset report "is required".
2 default false fills the attribute in automatically if the caller does not supply it.

Now we can create a new task without providing a value for is_done:

iex> App.ToDoList.create_task(%{content: "Mow the lawn"})
{:ok,
 %App.ToDoList.Task{
   id: "07d5b3f1-b960-4390-8980-5e731251d7af",
   content: "Mow the lawn",
   priority: nil,
   is_done: false,
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}

Limiting the accept list

default_accept is also the place to refuse input on specific attributes. Say we want users to set :content and :priority, but not flip :is_done through a create or update (we would update that through a dedicated action such as :complete):

lib/app/to_do_list/task.ex
  actions do
    default_accept [:content, :priority] (1)
    defaults [:create]
  end
1 :is_done is intentionally omitted.

If someone tries anyway, Ash returns a clear error:

$ iex -S mix
Compiling 2 files (.ex)
Erlang/OTP 28 [erts-16.2.2] [...]

Interactive Elixir (1.20.0-rc.4) [...]
iex(1)> App.ToDoList.create_task(%{content: "Mow the lawn", is_done: true})
{:error,
 %Ash.Error.Invalid{
   errors: [
     %Ash.Error.Invalid.NoSuchInput{
       resource: App.ToDoList.Task,
       action: :create,
       input: :is_done,
       inputs: MapSet.new([:priority, :content, "content", "priority"]),
       did_you_mean: [],
       class: :invalid,
       ...
     }
   ],
   ...
 }}
iex(2)>