Getting Started with Ash Framework

Getting Started with Ash Framework

Rebecca Le

Rebecca Le

20 May 2025 · 5 min read

As modern applications grow in complexity, developers often find themselves writing repetitive code to handle common patterns like CRUD operations, authorisation, and data relationships. Ash Framework addresses these challenges with a declarative approach, that emphasises what your data is rather than the mechanics of how to interact with it.

This guide will walk you through setting up your first Ash project, understanding its core concepts, and leveraging the Igniter toolkit to accelerate your development.

Understanding Ash Framework

Before diving into code, let's understand what makes Ash unique:

  1. Declarative Design: Describe the behaviours of your app, and let Ash handle the implementation details.
  2. Built-in Authorization: First-class support for actors, permissions, and policies at the resource level.
  3. Extensible Architecture: The Ash ecosystem provides first-party plugins for PostgreSQL, Phoenix, Oban, authentication, state machines, and more. Or you can write your own!
  4. Reusable Actions: Define actions once and reuse them across multiple interfaces, promoting consistency while eliminating duplication.

Ash allows you to focus on modelling your domain correctly, while it handles the implementation details of persistence, validation, and authorisation.

Setting up your first project

Ash isn’t limited to only being used in web applications, but for our demo we’ll create a new Phoenix app and test it out.

Prerequisites

Before starting, make sure you have:

Creating a new project

Start by creating a new Phoenix project. We’ll create one called ash_demo:

$ mix phx.new ash_demo
$ cd ash_demo
$ mix ecto.setup

Installing igniter_new provides access to the igniter.install Mix task, which adds dependencies and runs setup scripts for them within your app. We can use it to install Ash and its PostgreSQL integration library, AshPostgres:

$ mix igniter.install ash ash_postgres

This will set up the two libraries, and replace the Ecto repo configuration in the app with an AshPostgres.Repo configuration instead.

Building your first resources

In Ash, the core unit is the resource. Resources are the nouns in your app, like Post , Comment or User, and are grouped together into domains that describe their responsibility, like Blog or Accounts.

Use Ash’s generators to generate new resources for a Post and a Comment, in a new Blog domain:

$ mix ash.gen.resource AshDemo.Blog.Post --extend postgres
$ mix ash.gen.resource AshDemo.Blog.Comment --extend postgres

Inside the resource in lib/ash_demo/blog/post.ex, fill out the basic details of a Post. This uses the attributes, relationships and actions DSLs:

defmodule AshDemo.Blog.Post do
  use Ash.Resource, # ...

  postgres do
    # ... 
  end

  actions do
    defaults [:create, :read, :update, :destroy]
    default_accept [:title, :body, :author_name]
  end

  attributes do
    uuid_primary_key :id

    attribute :title, :string do
      allow_nil? false
      constraints [min_length: 3, max_length: 100]
    end

    attribute :body, :string do
      allow_nil? false
      constraints [min_length: 10]
    end

    attribute :author_name, :string do 
      allow_nil? false
    end

    create_timestamp :inserted_at
    update_timestamp :updated_at
  end

  relationships do
    has_many :comments, AshDemo.Blog.Comment
  end
end

You can fill out similar details for the Comment resource in lib/ash_demo/blog/comment.ex:

defmodule AshDemo.Blog.Comment do
  use Ash.Resource, # ...

  postgres do
    # ...
  end

  actions do
    defaults [:create, :read, :update, :destroy]
    default_accept [:content, :author_name, :post_id]
  end

  attributes do
    uuid_primary_key :id

    attribute :content, :string do 
      allow_nil? false 
    end

    attribute :author_name, :string do 
      allow_nil? false
    end

    create_timestamp :inserted_at
    update_timestamp :updated_at
  end

  relationships do
    belongs_to :post, AshDemo.Blog.Post do
      allow_nil? false
    end
  end
end

Generating database migrations

AshPostgres can generate migrations for you, based on the state of your resources. No more manually writing them when you change your schema!

$ mix ash.codegen create_blog_tables
$ mix ash.migrate

Interacting with your resources

We added four actions to each resource, representing CRUD behaviour.

Read actions

For read actions, you can use the Ash.Query API to read, filter, sort, and aggregate data.

iex(1)> AshDemo.Blog.Post
        |> Ash.Query.for_read(:read)
        |> Ash.Query.load(:comments)
        |> Ash.read!()
{:ok, [...]}

Create, update, delete actions

For create , update and destroy actions, you can use Ash.Changeset functions to build changesets, validate data, and update the database. Each action can set the attributes listed in the default_accept list in the resource.

iex(2)> # Create a new post
        AshDemo.Blog.Post
        |> Ash.Changeset.for_create(:create, %{
          title: "My First Post", 
          body: "Hello, Ash Framework!", 
          author_name: "Rebecca"
        })
        |> Ash.create()
{:ok, %AshDemo.Blog.Post{title: "My First Post", ...}}

iex(3)> {:ok, post} = v()
{:ok, %AshDemo.Blog.Post{...}}

iex(4)> # Update an existing post
        post
        |> Ash.Changeset.for_update(:update, %{
          title: "Not Actually My First Post!"
        })
        |> Ash.update()
{:ok, %AshDemo.Blog.Post{title: "Not Actually My First Post", ...}}

iex(5)> # Create a comment for a post
        AshDemo.Blog.Comment
        |> Ash.Changeset.for_create(:create, %{
          content: "Great post!", 
          post_id: post.id, 
          author_name: "Ben"
        })
        |> Ash.create()
{:ok, %AshDemo.Blog.Comment{...}}

Customising generated resources

To do more than basic CRUD, you can customise your resources. You can add attribute validations, action changes and preparations, and more. (These are also configured with the Ash.Resource DSL - bookmark this page in the docs, you’ll use it a lot!)

# In the Post resource
actions do 
  # Remove `:create` and `:read` from here
  defaults [:update, :destroy]
  default_accept [:title, :body, :author_name]

  # And add a new `create` action
  create :create do 
    # Only Rebecca and Zach can create posts
    validate attribute_in(:author_name, ["Rebecca", "Zach"])

    # But we'll say Ben wrote them all anyway
    change set_attribute(:author_name, "Ben")
  end

  # And a new `read` action
  read :read do 
    # Always load the comments when reading posts
    prepare build(load: [:comments])
  end
end
iex(6)> AshDemo.Blog.Post
        |> Ash.Changeset.for_create(:create, %{
          author_name: "Mike", 
          title: "Mike's Post", 
          body: "Hello world!"
        })
        |> Ash.create()
{:error, 
 %Ash.Error.Invalid{
   errors: [
     %Ash.Error.Changes.InvalidAttribute{
       field: :author_name,
       message: "must be in %{list}",
       vars: [list: ["Rebecca", "Zach"]],
       # ...

iex(7)> AshDemo.Blog.Post
        |> Ash.Changeset.for_create(:create, %{
          author_name: "Rebecca", 
          title: "Who wrote this?", 
          body: "I don't know!"
        })
        |> Ash.create()
{:ok, %AshDemo.Blog.Post{author: "Ben", ...}}

iex(8)> AshDemo.Blog.Post
        |> Ash.Query.for_read(:read)
        |> Ash.read()
{:ok, [%AshDemo.Blog.Post{comments: [...], ...}, ...]}

Calling actions as functions

To simplify calling actions, you can define code interfaces for them. These can have extra customisations added, such as automatically applying filters or extra loads, or mapping action arguments to function arguments.

# In the Post resource
code_interface do 
  define :create, args: [:author_name]
  define :get_by_id, action: :read, get_by: :id
end
iex(9)> {:ok, post} = AshDemo.Blog.Post.create(
                        "Rebecca", 
                        %{title: "New Post!", body: "This is so cool"}
                      )
{:ok, %AshDemo.Blog.Post{name: "Ben", ...}}

iex(10)> AshDemo.Blog.Post.get_by_id(post.id)
{:ok, %AshDemo.Blog.Post{...}}

Integrating with Phoenix

To use Ash resources in your Phoenix controllers and liveviews, you can use the ash_phoenix package. Install it with Igniter:

$ mix igniter.install ash_phoenix

This gives access to AshPhoenix.Form helpers, for working with forms built for resource actions:

# In a liveview mount, or a controller action
form = AshPhoenix.Form.for_create(AshDemo.Blog.Post, :create)

# In a liveview change event handler, or a controller action
form = AshPhoenix.Form.validate(form, params_from_form)

# In a liveview submint handler, or a controller action
case AshPhoenix.Form.submit(form, params: params_from_form) do
  {:ok, record} ->
    # The save was successful - redirect, show a flash message, etc.

  {:error, form} ->
    # The same wasn't successful - `form` contains all of the errors to show on the form

An AshPhoenix.Form can be used in the same way an Ecto.Changeset can, with your existing function components for forms and inputs.

For showing data, read actions can be used, the same way context functions would be without Ash.

Conclusion

What we've looked at here really only scratches the surface of what's possible with Ash. As you dig deeper, you'll see how it can handle increasingly complex business requirements while keeping your codebase clean and maintainable.

I encourage you to check out the official Ash documentation, join the community in the Ash Framework Discord, read the Ash book, and experiment with the various extensions available in the ecosystem.

Happy coding!

Need help with Ash Framework?

Getting started with Ash Framework? We can help. Whether it's supporting your team in up-skilling, or getting stuck into building an ambitious web app with you. Find out more about our Ash Framework services.

Rebecca Le

WRITTEN BY

Rebecca Le

Rebecca is a member of the Ash Framework core team. She is a co-author of Ash Framework and Rails 4 in Action, and has way too many years of experience in building web applications.

Want to read more?

The latest news, articles, and resources, sent to your inbox occasionally.