<!--
SPDX-FileCopyrightText: 2026 Nduati Kuria
SPDX-License-Identifier: MIT
-->
<img src="https://github.com/NduatiK/ash_gleam/blob/main/logo.png?raw=true" alt="Logo" width="300"/>

[](https://opensource.org/licenses/MIT)
[](https://hex.pm/packages/ash_gleam)
[](https://hexdocs.pm/ash_gleam)
# AshGleam
**Type-safe Gleam interop for Ash resources**
AshGleam bridges Elixir's [Ash framework](https://ash-hq.org) and [Gleam](https://gleam.run) in two directions:
- **Elixir → Gleam (generated bridge modules):** Generate typed Gleam modules from your Ash resources so Gleam code can call Ash actions with full compile-time type safety.
- **Gleam → Elixir (Gleam actions):** Wire compiled Gleam functions as the implementation of Ash generic actions, letting you run Gleam logic in your Elixir backend.
## How it works
AshGleam generates Gleam source files from your Ash resource definitions. Each resource becomes a typed Gleam record. Each exported Ash action becomes a Gleam module with a builder pattern and an `@external` call back into a generated Elixir bridge module.
```
mix ash_gleam.codegen
```
The generated files live in a configurable output directory (default `lib/ash_gleam/generated/src`) and are compiled alongside your regular Gleam sources.
## Supported types
| Elixir / Ash type | Gleam type |
|---|---|
| `:string`, `:uuid` | `String` |
| `:integer` | `Int` |
| `:boolean` | `Bool` |
| `:float`, `:decimal` | `Float` |
| `{:array, t}` | `List(T)` |
| Any resource with `AshGleam.Resource` | Named record type |
| `AshSumType` module | Generated Gleam union type |
| `allow_nil?: true` on any of the above | `Option(T)` |
Embedded resources (`:embedded` data layer) with the `AshGleam.Resource` extension are fully supported, including as array fields (`{:array, EmbeddedResource}`).
## Calling Ash from Gleam
### 1. Mark resources for Gleam type generation
Add `AshGleam.Resource` to your resource and declare a `gleam` block:
```elixir
defmodule MyApp.Todo do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshGleam.Resource]
gleam do
type_name "Todo" # required — the Gleam type name
module_name "todo_item" # optional — overrides the generated file name
end
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false, public?: true
attribute :completed, :boolean, default: false, public?: true
end
# ...
end
```
Only `public?: true` attributes are included in the generated Gleam type.
### 2. Export actions through a domain
Add `AshGleam.Domain` to your domain and list the actions you want to expose inside the `gleam` DSL:
```elixir
defmodule MyApp.Domain do
use Ash.Domain,
otp_app: :my_app,
extensions: [AshGleam.Domain]
resources do
resource MyApp.Todo
end
gleam do
ffi do
resource MyApp.Todo do
action :list_todos, :read
action :create_todo, :create
action :get_todo, :get
action :destroy_todo, :destroy
end
end
end
end
```
### 3. Run codegen
```bash
mix ash_gleam.codegen
```
### 4. Use from Gleam
Each exported action becomes its own Gleam module. The first name in `action :list_todos, :read` becomes the generated module name.
**Listing:**
```gleam
import myapp/generated/src/list_todos
import myapp/generated/src/todo_item.{type TodoFilter, type TodoSort}
pub fn fetch_incomplete(): Result(List(Todo), String) {
list_todos.new()
|> list_todos.filter([todo_item.completed_eq(False)])
|> list_todos.sort([todo_item.title_asc()])
|> list_todos.limit(option.Some(10))
|> list_todos.run()
}
```
**Creating:**
```gleam
import myapp/generated/src/create_todo
pub fn add_todo(title: String): Result(Todo, String) {
create_todo.new(title, False, 1)
|> create_todo.run()
}
```
**Reading a single record:**
```gleam
import myapp/generated/src/get_todo
pub fn find_todo(id: String): Result(Todo, String) {
get_todo.new(id)
|> get_todo.run()
}
```
**Deleting:**
```gleam
import myapp/generated/src/destroy_todo
import myapp/generated/src/todo_item.{type Todo}
pub fn remove_todo(todo_item: Todo): Result(Bool, String) {
destroy_todo.DestroyTodo(todo_item)
|> destroy_todo.run()
}
```
## Gleam actions — calling Gleam from Elixir
The `AshGleam.Actions` extension lets you implement Ash generic actions in Gleam. The Gleam function is compiled to BEAM and called directly — no HTTP, no serialization overhead.
### 1. Write a Gleam function
```gleam
// todo_logic.gleam
import myapp/generated/src/todo_item.{type Todo, Todo}
pub fn mark_completed(item: Todo) -> Todo {
Todo(..item, completed: True)
}
pub fn safe_add(a: Int, b: Int) -> Result(Int, String) {
case a < 0 || b < 0 {
True -> Error("negative numbers not allowed")
False -> Ok(a + b)
}
}
```
### 2. Wire it to an Ash action
Add `AshGleam.Actions` to your resource and declare the action with a MFA reference:
```elixir
defmodule MyApp.Todo do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshGleam.Resource, AshGleam.Actions]
# ...
gleam do
actions do
# Takes a Todo, returns a Todo
action :mark_completed, __MODULE__ do
argument :todo, __MODULE__, allow_nil?: false
update? true
run &:todo_logic.mark_completed/1
end
# Takes two integers, returns Result(Int, String)
action :safe_add, :integer do
argument :a, :integer, allow_nil?: false
argument :b, :integer, allow_nil?: false
run &:todo_logic.safe_add/2
end
# Returns a reusable sum type
action :next_mark, MyApp.Mark do
run &:todo_logic.next_mark/0
end
end
end
end
```
## Reusable named sum types
You can define Gleam-facing sum types once on the Elixir side and reuse them in resource attributes and `gleam.actions`.
### Nullary sum types
```elixir
defmodule MyApp.Mark do
use AshSumType
variant :x
variant :o
variant :empty
end
defmodule MyApp.Board do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshGleam.Resource, AshGleam.Actions]
gleam do
type_name "Board"
actions do
action :next_mark, MyApp.Mark do
run &:board_logic.next_mark/0
end
end
end
attributes do
attribute :next_mark, MyApp.Mark, public?: true
end
end
```
AshGleam will generate one shared Gleam type module for `MyApp.Mark` and reuse it everywhere instead of generating one type per field or action:
```gleam
pub type Mark {
X
O
Empty
}
```
### Sum types with payloads
Use `AshSumType` variants with carried fields when you want constructors that hold values:
```elixir
defmodule MyApp.LookupOutcome do
use AshSumType
variant :found do
field :value, MyApp.Todo, allow_nil?: false
end
variant :missing do
field :error, :string, allow_nil?: false
end
end
```
That maps to a generated Gleam union like:
```gleam
pub type LookupOutcome {
Found(Todo)
Missing(String)
}
```
`AshSumType` values stay regular sum-type data across the boundary. Nullary variants map to atoms on the Elixir side, and payload variants map to tagged tuples in declared field order. Action `Result(T, String)` behavior is unchanged: if a Gleam action returns `Result`, AshGleam still treats `{:ok, value}` / `{:error, error}` as the action success/error channel.
### 3. Call it through Ash
Scalar-returning and non-update Gleam actions can still be called directly through the generated resource functions:
```elixir
todo = MyApp.Todo |> Ash.Changeset.for_create(:create, %{title: "Ship it"}) |> Ash.create!()
{:ok, 5} = MyApp.Todo.safe_add(%{a: 2, b: 3})
{:error, _} = MyApp.Todo.safe_add(%{a: -1, b: 3})
```
Gleam functions that return `Result(T, String)` map to `{:ok, value}` / `{:error, %Ash.Error.Unknown{}}`. Functions that return a bare value are always wrapped in `{:ok, value}`.
For Gleam actions marked `update? true`, prefer `AshGleam.Changeset.for_update/4` when you want to inspect or modify the changeset before persisting:
```elixir
todo =
MyApp.Todo
|> Ash.Changeset.for_create(:create, %{title: "Ship it"})
|> Ash.create!()
{:ok, changeset} =
AshGleam.Changeset.for_update(todo, :mark_completed, %{}, action: :update)
persisted = Ash.update!(changeset)
persisted.completed #=> true
```
If you want a code interface that persists the update for you, configure it on the domain:
```elixir
defmodule MyApp.Domain do
use Ash.Domain,
otp_app: :my_app,
extensions: [AshGleam.Domain]
resources do
resource MyApp.Todo
end
gleam do
code_interface do
resource MyApp.Todo do
define_gleam_update :mark_completed, action: :update
end
end
end
end
```
That generates domain functions like `mark_completed/1-3` and `mark_completed!/1-3`:
```elixir
todo =
MyApp.Todo
|> Ash.Changeset.for_create(:create, %{title: "Ship it"})
|> Ash.create!()
{:ok, updated} = MyApp.Domain.mark_completed(todo)
updated.completed #=> true
```
`AshGleam.Diff.resource_changes/2` is still available when you need the raw diff, but it is no longer the recommended primary workflow for update-style Gleam actions.
## Embedded resources
Resources with the `:embedded` data layer work as field types in other resources. The embedded resource gets its own Gleam type and is imported automatically in the parent resource's generated file.
```elixir
defmodule MyApp.Tag do
use Ash.Resource,
domain: MyApp.Domain,
data_layer: :embedded,
extensions: [AshGleam.Resource]
gleam do
type_name "Tag"
end
attributes do
attribute :label, :string, allow_nil?: false, public?: true
attribute :color, :string, allow_nil?: false, public?: true
end
end
defmodule MyApp.Todo do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshGleam.Resource]
gleam do
type_name "Todo"
end
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false, public?: true
attribute :tags, {:array, MyApp.Tag}, allow_nil?: false, default: [], public?: true
end
end
```
The generated `todo_item.gleam` will import `tag.gleam` and use `List(Tag)` as the field type. All marshalling through Gleam actions and generated bridge calls handles the nested types transparently.
## Gleam functions calling back into Elixir
Gleam actions can use the generated bridge modules to call Ash actions, enabling patterns where Gleam orchestrates Ash reads or writes:
```gleam
import myapp/generated/src/first_completed_todo
pub fn get_first_completed() -> Todo {
first_completed_todo.new()
|> first_completed_todo.run()
}
```
## Configuration
In `config/config.exs` (or environment-specific config):
```elixir
config :ash_gleam,
output: "lib/my_app/generated" # default: "lib/ash_gleam/generated"
```
The generated output directory must be under a `src/` parent so that Gleam's module path resolution works. The module prefix used in `import` statements is derived from the path automatically.
## Installation
### With Igniter (recommended)
```bash
mix igniter.install ash_gleam
# If testing locally:
mix igniter.install ash_gleam@path:..
```
This automatically configures your `mix.exs` with all the settings required by
[MixGleam](https://github.com/gleam-lang/mix_gleam): compilers, `erlc_paths`,
`erlc_include_path`, `prune_code_paths`, the `deps.get` alias, and the
`gleam_stdlib` / `gleeunit` dependencies. It also creates the `src/` directory
and adds `build/` to your `.gitignore`.
You will still need to install the Gleam compiler and the MixGleam archive:
```bash
# Install the Gleam compiler — see https://gleam.run/getting-started/installing-gleam.html
# Install the MixGleam Mix archive
mix archive.install hex mix_gleam
```
### Manual setup
Add `ash_gleam` to your dependencies:
```elixir
# mix.exs
defp deps do
[
{:ash_gleam, "~> 0.1"},
{:gleam_stdlib, "~> 0.34 or ~> 1.0"},
{:gleeunit, "~> 1.0", only: [:dev, :test], runtime: false}
]
end
```
Then follow the [MixGleam README](https://github.com/gleam-lang/mix_gleam) to
configure your project:
```elixir
# mix.exs
@app :my_app
def project do
[
app: @app,
# ...
archives: [mix_gleam: "~> 0.6"],
compilers: [:gleam | Mix.compilers()],
aliases: [
"deps.get": ["deps.get", "gleam.deps.get"]
],
erlc_paths: [
"_build/dev/erlang/#{@app}/_gleam_artefacts",
"_build/dev/erlang/#{@app}/build"
],
erlc_include_path: "_build/dev/erlang/#{@app}/include",
prune_code_paths: false
]
end
```
Create a `src/` directory for your Gleam source files and add `build/` to your
`.gitignore`.
## Requirements
- Elixir 1.15+
- Ash 3.0+
- Gleam (with `mix_gleam` configured)
## Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for any new behaviour
4. Run `mix test` and `mix format`
5. Open a pull request
## License
MIT — see [LICENSES/MIT.txt](https://github.com/NduatiK/ash_gleam/blob/main/LICENSES/MIT.txt).
## Links
- **Hex**: [https://hex.pm/packages/ash_gleam](https://hex.pm/packages/ash_gleam)
- **Docs**: [https://hexdocs.pm/ash_gleam](https://hexdocs.pm/ash_gleam)
- **Issues**: [https://github.com/NduatiK/ash_gleam/issues](https://github.com/NduatiK/ash_gleam/issues)