New: Temporal Resources

Model your domain,
derive the rest

Discover Ash Features

Resources & Actions

Step 1 of 9

Resources & Actions are the core abstraction in Ash. Actions are fully typed and introspectable (your application can examine them at runtime). This means extensions can automatically understand and build on top of them.

lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
  use Ash.Resource
  actions do
    action :reading_time, :integer do
      argument :content, :string,
        allow_nil?: false

      run fn input, _ ->
        words =
          input.arguments.content
          |> String.split()
          |> length()

        {:ok, div(words, 200) + 1}
      end
    end
  end
end

Functional Interface

MyApp.Blog.Post.reading_time("content string")
Takes:
content: String
Returns:
Integer (minutes)
Example:
reading_time("A long blog post...") → 3

Persistence

Step 2 of 9

Now let's add state to support persistent storage, while keeping our existing behavior. Your resource now combines behavior and state. The original action still works exactly the same, plus you can create and persist posts.

lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
  use Ash.Resource
    data_layer: AshPostgres.DataLayer

  postgres do
    table "posts"
    repo MyApp.Repo
  end

  attributes do
    uuid_primary_key :id
    attribute :title, :string,
      allow_nil?: false
    attribute :content, :string
    attribute :status, :atom,
      constraints: [
        one_of: [:draft, :published]
      ]

    timestamps()
  end
  # ...
end

PostgreSQL Table: posts

ColumnType
iduuid
titletext
contenttext
statustext
created_attimestamp
updated_attimestamp

GraphQL

Step 3 of 9

Add a full GraphQL API with minimal configuration. The extension automatically understands your existing actions and generates queries, mutations, and types.

lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
  use Ash.Resource
    extensions: [AshGraphql.Resource]

  graphql do
    type :post

    queries do
      get :post, :read
      list :posts, :read
    end

    mutations do
      create :create_post, :create
      update :update_post, :update
      destroy :delete_post, :destroy
    end
  end
  # ...
end

GraphQL API

Query
query GetPost($id: ID!) {
  post(id: $id) {
    id
    title
  }
}
Mutation
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
  }
}

JSON:API

Step 4 of 9

Add a REST JSON:API alongside your GraphQL API. All API extensions work on top of your resources and actions. Ash makes it trivial to expose your domain through multiple API standards, letting clients choose the interface that works best for them.

lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
  use Ash.Resource
    extensions: [
      AshGraphql.Resource,
      AshJsonApi.Resource
    ]

  json_api do
    type "post"

    routes do
      base "/posts"
      get :read
      index :read
      post :create
      patch :update
    end
  end
  # ...
end

JSON:API REST Endpoints

  • GET /posts
  • GET /posts/:id
  • POST /posts
  • PATCH /posts/:id
Example Response
{
  "data": {
    "type": "post",
    "id": "123",
    "attributes": {
      "title": "My Post",
      "content": "Post content..."
    }
  }
}

Policies

Step 5 of 9

Add fine-grained authorization with policies. Control who can perform which actions and which data they can see. Policies can forbid actions or filter data transparently for reads, preventing enumeration attacks.

lib/my_app/support/ticket.ex
defmodule MyApp.Support.Ticket do
  use Ash.Resource
    extensions: [
      # ...
    ],
    authorizers: [Ash.Policy.Authorizer]

  policies do
    # Only admins can create tickets
    policy action_type(:create) do
      authorize_if expr(
        ^actor(:role) == :admin
      )
    end

    # Users can only read their own tickets
    policy action_type(:read) do
      authorize_if expr(
        user_id == ^actor(:id)
      )
    end
  end
  # ...
end

Authorization Policies

Create Policy
policy action_type(:create) do
  authorize_if expr(
    ^actor(:role) == :admin
  )
end
Forbidden Action
MyApp.Support.open_ticket!(
  actor: non_admin
)
# Forbidden
Read Policy
policy action_type(:read) do
  authorize_if expr(
    user_id == ^actor(:id)
  )
end
Filtered Read
MyApp.Tickets.list_tickets(
  actor: user
)
# SQL: SELECT * FROM tickets
# WHERE user_id = $1

Encryption

Step 6 of 9

Add encryption at rest with our Cloak integration. Your post content is now automatically encrypted when stored and decrypted when read, with no changes to your existing API or business logic. The encryption is completely transparent to your application code.

lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
  use Ash.Resource
    extensions: [
      AshGraphql.Resource,
      AshJsonApi.Resource,
      AshCloak.Resource
    ]

  cloak do
    vault MyApp.Vault
    # Automatically encrypt content
    attributes [:content]
  end
  # ...
end

Encryption at Rest

Application "Hello World"
Database AES256:B64:IV...

Content field automatically encrypted/decrypted

AI Tools

Step 7 of 9

Add AI-powered actions and expose your domain as tools for LLMs. Ash AI lets you create prompt-backed actions that delegate to AI models, and exposes your existing actions as tools that AI agents can use. Perfect for building AI-powered features or MCP (Model Context Protocol) servers.

lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
  use Ash.Resource
    extensions: [
      #...,
      AshAi
    ]

  actions do
    # AI-powered action
    action :analyze_sentiment, :atom do
      constraints [
        one_of: [:positive, :negative]
      ]

      argument :content, :string,
        allow_nil?: false

      run prompt(
        ChatOpenAI.new!(
          %{model: "gpt-4o"}
        )
      )
    end
  end
  #...
end

AI-Powered Actions & Tools

Prompt-Backed Action
analyze_sentiment(content: "Great post!")
→ :positive

MCP Tools Exposed

  • create_post
  • read_posts
  • update_post
  • analyze_sentiment

Authentication

Step 8 of 9

Add complete user authentication with password and OAuth strategies. AshAuthentication provides built-in support for password authentication, OAuth providers (GitHub, Google, etc.), magic links, and more.

lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
  use Ash.Resource
    extensions: [AshAuthentication]

  authentication do
    strategies do
      password do
        # ...
      end

      magic_link do
        # ...
      end
    end
  end
  # ...
end

User Authentication

Authentication Strategies

  • Password
  • Magic Link
  • Api Key
  • GitHub
  • Google
  • Slack
  • Custom

Background Jobs

Step 9 of 9

Add background job processing with AshOban. Define triggers that run periodically for records matching conditions, and scheduled actions that run on cron schedules. Perfect for notifications, data processing, and maintenance tasks.

lib/my_app/blog/post.ex
defmodule MyApp.Blog.Post do
  use Ash.Resource
    extensions: [
      # ...
      AshOban
    ]

  oban do
    triggers do
      trigger :publish do
        where expr(state == :pending)
        scheduler_cron "@hourly"
      end
    end
    scheduled_actions do
      schedule :cleanup_old_drafts,
        "@daily"
    end
  end
  # ...
end

Background Job Processing

Triggers

Run actions for records matching conditions on a schedule

publish Runs hourly to publish pending posts

Scheduled Actions

Run generic actions on cron schedules

cleanup_old_drafts Runs daily to clean up old draft posts

The Ash Ecosystem

Powerful extensions that integrate seamlessly with your resources.

Proud Partner

Erlang Ecosystem Foundation (opens in a new tab)

The EEF is the 501(c)(3) non-profit that stewards the BEAM ecosystem we all build on. Their working groups make Elixir, Erlang, and the wider BEAM safer and more sustainable for everyone.

  • Securing the Ecosystem

    The EEF Security Working Group identifies vulnerabilities, publishes guidance, and funds independent audits — including the recent Hex.pm security audit that protects every package in the ecosystem, Ash included.

  • Supporting the Community

    Working groups for build tools, observability, education, ML, and infrastructure — plus fellowships, stipends, conferences, and the documentation that newcomers and experts rely on every day.

  • Becoming a Member

    Individuals and companies can join as members starting at modest annual dues. Membership funds the working groups directly, gives you a voice in governance, and keeps the BEAM independent and community-driven.

Learn more at erlef.org (opens in a new tab) — every contribution and member helps secure and sustain the BEAM.

One Command to a Working App

Pick a preset or choose features one by one. The installer uses Igniter to set it all up.

sh <(curl 'https://ash-hq.org/install/lemonade_stand') && cd lemonade_stand