Introduction to Ash framework


Ash helps to keep business logic in one place. You can model your domain with resources and actions, generate database migrations, and add validations and filtering without scattering logic across your codebase.
Every developer who has created software professionally, and not only in Elixir, has encountered problems as their codebase expands. One of the problems is business logic becoming scattered and fragmented across an application. This fragmentation leads to data being handled inconsistently across different modules and creates a heavy maintenance burden, where a single change requires updates in multiple disconnected locations. On top of that we know that every developer builds his code slightly differently which can also lead to some inconsistencies and inefficiencies.
This is where Ash Framework enters the scene.
Ash Framework is an application framework for Elixir that focuses on defining your domain and business rules in a clear and declarative way. Instead of spreading logic across many layers of the application, Ash encourages you to describe what your system is and what it can do using well-defined Resources and Actions. It provides a consistent structure for handling data, validation, authorization, and APIs, helping teams build applications that are easier to understand, extend, and maintain as they grow.
To understand how Ash achieves this, let’s start with its most fundamental concepts.
Table of contents
- Demo app
- Resources – the core building block of Ash
- Generating database migrations
- Basic CRUD actions
- Custom validations
- Calculations
- Filtering with Action Arguments
- Conclusion
Demo app
For the purposes of this article, we will create a very simple application to show the basic Ash concepts. Let's say our app will be a storage for articles. I expect that you already have Elixir installed and also PostgreSQL database. We will create our app using Igniter:
```
mix archive.install hex igniter_new
mix igniter.new article_hub --install ash,ash_postgres && cd article_hub
```
Igniter created our app together with Ash & and AshPostgres in it for us. Let's also create a local database by running:
mix ecto.createResources
Resource is a central concept in Ash. A resource represents a core part of your application’s domain the data and the rules that surround it in a single place. Instead of scattering logic across multiple modules, a resource lets you define what an entity is, what actions can be performed on it, and how it should be validated and authorized. Understanding resources is the first step to building maintainable and consistent applications with Ash. Let's generate our first resource and domain in our app to check how it works.
mix ash.gen.resource ArticleHub.Contents.Article --extend postgres
It created two files for us. article.ex inside contents folder which is a resource file and also contents.ex file which is domain file. First let's take a look at resource file:
defmodule ArticleHub.Contents.Article do
use Ash.Resource,
otp_app: :article_hub,
domain: ArticleHub.Contents,
data_layer: AshPostgres.DataLayer
postgres do
table "articles"
repo ArticleHub.Repo
end
end
For now, our resource file is empty - we only have the minimum information: the repo and table name. The resource is connected to Postgres because we used --extend postgres in our generator.
Let's add some new attributes to our resource. To add new attributes add new block in the resource named attributes:
#...
attributes do
integer_primary_key :id
attribute :name, :string do
allow_nil? false
end
attribute :url, :string do
allow_nil? false
end
attribute :description, :string do
constraints max_length: 500,
min_length: 10
allow_nil? true
end
create_timestamp :inserted_at
update_timestamp :updated_at
end
#...
As shown above, we added several additional fields: an integer primary key, name and url fields that cannot be null, and a description field with additional constraints. There are many other configuration options available for attributes. For more details, see the framework documentation, particularly the Resource Attributes section.
Generating database migrations
Now that we’ve added the fields, our resource is still just an Elixir module. It defines the configuration for the database, but the actual table doesn't exist yet. We’ll use a generator to create a database migration based on this resource. This follows the standard Ash migration workflow. Let's run:
mix ash.codegen create_articles
Check your migration inyour_app/priv/repo/migrations folder with database migrations. It should look like this:
# priv/repo/migrations/..._create_articles.exs
def up do
create table(:articles, primary_key: false) do
add(:id, :bigserial, null: false, primary_key: true)
add(:name, :text, null: false)
add(:url, :text, null: false)
add(:description, :text)
add(:inserted_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
)
add(:updated_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
)
end
end
def down do
drop(table(:articles))
end
You should see there a new migration that creates a new table with fields that we added to our resource. Now run migration to create table using this command:
mix ash.migrate
When using Ecto, we would add new fields to our schema and then generate a blank migration, adding the new fields to the migration to mirror the changes in the schema. It works differently in Ash. Here, we add fields only to the Resource, and then generate a migration based on the previously defined Resource. In this case, the Resource file is the source of truth, and every time we make a change - such as adding a new field - we need to generate a new migration using ash.codegen. This ensures that your database stays up to date with the Resource file..
Basic CRUD actions
Let’s start by defining a basic create action for our app. To do this, open article.ex and add an actions block like the one below:
# ...
actions do
create :create do
accept [:name, :description, :url]
end
end
# ...
A basic action requires an action type (create, read, update, or destroy) and a name - the name can be any atom you like. We also added the accept macro, where we define which fields are accepted by the action. For now, using this action, our articles will be created via a changeset like the one below. Let’s run the app and test it by starting iex -S mix and then:
ArticleHub.Contents.Article
|> Ash.Changeset.for_create(:create, %{
name: "Ash Framework docs",
description: "Documentation about Ash",
url: "https://hexdocs.pm/ash/readme.html"
})
|> Ash.create()
We can see that our article was created:
{:ok,
%ArticleHub.Contents.Article{
id: 1,
name: "Ash Framework docs",
url: "https://hexdocs.pm/ash/readme.html",
description: "Documentation about Ash",
...
}}
If you try to create an article without a URL, you will get an error:
ArticleHub.Contents.Article
|> Ash.Changeset.for_create(:create, %{
name: "Ash Framework docs",
description: "Documentation about Ash",
url: nil
})
|> Ash.create()
{:error,
%Ash.Error.Invalid{
bread_crumbs: ["Error returned from: ArticleHub.Contents.Article.create"],
changeset: "#Changeset<>",
errors: [
%Ash.Error.Changes.Required{
field: :url,
type: :attribute,
resource: ArticleHub.Contents.Article,
splode: Ash.Error,
bread_crumbs: ["Error returned from: ArticleHub.Contents.Article.create"],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
}}
It works perfectly - we get an error because of the allow_nil? false option on the url attribute.
You don’t need to manually create a changeset every time. You can create a code interface and call the action directly as a function. Let’s give it a try. Go to the contents.ex file and add this macro:
#...
resources do
resource ArticleHub.Contents.Article do
define :create_article, action: :create
end
end
#...
Recompile your project and now you can run:
ArticleHub.Contents.create_article(%{name: "Elixir", url: "https://elixir-lang.org/"})
You can see that now we connected our domain function to the create action of the resource.
Let's now define the rest of the basic actions. Add proper actions to you resource file article.ex:
#...
actions do
create :create do
accept [:name, :description, :url]
end
read :read do
primary? true
end
update :update do
accept [:name, :description, :url]
end
destroy :destroy do
end
end
#...
and then define code interface definitions:
#...
resources do
resource ArticleHub.Contents.Article do
define :create_article, action: :create
define :read_articles, action: :read
define :get_article_by_id, action: :read, get_by: :id
define :update_article, action: :update
define :delete_article, action: :destroy
end
end
#...
You can test those functions and play around with them. Check official docs for more information about actions.
At the end you don't need to define all of those actions. If you want default actions you can just go with:
#...
actions do
defaults [:create, :read, :update, :destroy]
default_accept [:name, :description, :url]
end
#...Custom validations
While simple constraints like max_length are defined directly on the attribute, more complex business rules are defined in a validations block. For our app, we want to ensure that every URL actually looks like a URL. Add this block to your ArticleHub.Contents.Article resource:
#...
validations do
validate match(:url, ~r/^https?:\/\/.*/), message: "must be a valid website address"
end
#...
By putting this in the validations block (outside of a specific action), this rule will automatically apply to both create and update actions. This is a simple regex that ensures the URL starts with either http:// or https://. Now, if you try to create an article with a malformed URL in iex:
ArticleHub.Contents.create_article(%{name: "Bad URL", url: "ftp://invalid"})
You should get an error from url validation block.
Filtering with Action Arguments
In Ash, instead of writing complex filters every time you fetch data, you can encapsulate that logic inside your resource using Arguments. This makes your API cleaner and ensures that the business logic for "searching" or "filtering" stays in one place. Let's add a search action to your article.ex that allows users to find articles by name:
#...
read :search do
argument :query, :string do
allow_nil? false
end
filter expr(contains(name, ^arg(:query)))
end
#...
Next, expose this action in your code interface within contents.ex:
#...
define :search_articles, action: :search, args: [:query]
#...
Now, searching for articles is as simple as calling a standard Elixir function: ArticleHub.Contents.search_articles("Ash").
Calculations
Often, you need data that isn't stored in the database but is calculated from other fields. For example, we might want to display just the name of the article together with insertion date. In Ash, you can use Calculations. These are powerful because they allow the database to do the calculation, which means you can even sort and filter by these derived values. Add this block to your article.ex:
#...
calculations do
calculate :title, :string, expr(name <> " (created on: " <> fragment("?::date", inserted_at) <> ")")
end
#...
To see this in action, we need to tell Ash to "load" the calculation, as it isn't loaded by default for performance reasons. Let's add load option to our read_articles function in contents.ex:
#...
define :read_articles, action: :read, default_options: [load: [:title]]
#...
This way every time we call read_articles function title field will be added to our data.
Conclusion
In this article, we've only scratched the surface of what Ash Framework can do. We've seen how to:
- Define a Resource as a single source of truth for our data.
- Generate Database Migrations directly from our resources.
- Create Actions and a Code Interface to interact with our domain.
- Add Validations and Calculations to keep our business logic encapsulated.
- Filtering with Actions Arguments.
By moving logic into the resource layer, Ash helps you build applications that are easy to maintain, highly consistent, and incredibly flexible. Check out the Ash documentation to dive deeper!
Want to power your product with Elixir? We’ve got you covered.
Related posts
Dive deeper into this topic with these related posts
You might also like
Discover more content from this category
In the rapidly evolving world of software development, choosing the right programming language is essential for the success and growth of your business.
Join Savannah Manning at Elixir Meetup #14 as she shares her journey of mastering Elixir and the resources that helped her. Discover valuable tools and tips to enhance your Elixir learning experience.
Elixir and its ecosystem is gradually becoming the go-to platform for many web application developers who want both performance and productivity without a tradeoff.



