id: UG-0001 title: Getting Started with AshUI audience: Application Developers status: Active owners: Ash UI Team last_reviewed: 2026-04-25 next_review: 2026-10-25 related_reqs: [REQ-RES-001, REQ-SCREEN-001, REQ-COMP-001, REQ-RENDER-001] related_scns: [SCN-004, SCN-021, SCN-041, SCN-061] related_guides: [UG-0002, UG-0003, UG-0004, UG-0005, DG-0001] diagram_required: true
AshUI lets you author UI as Ash resources instead of hand-building a long-lived
widget tree in one stored document. In the current architecture, screen and
element resource modules are the authored source of truth, AshUI.Resource.Authority
persists a snapshot of that graph into the configured Screen storage resource,
and the compiler/runtime stack turns that snapshot back into canonical renderer
input for LiveView or other adapters.
This is the guide to read first because it explains the end-to-end shape before you dive into widget details, bindings, or authorization.
Before reading this guide, you should:
- Know basic Ash resource and domain concepts.
- Be comfortable with Phoenix LiveView.
- Understand that AshUI needs a
:current_userassign at runtime.
AshUI is easiest to reason about as a pipeline:
flowchart LR
Screen["screen resource module"]
Elements["element resource modules"]
Authority["AshUI.Resource.Authority"]
Persisted["configured Screen storage record"]
Compiler["AshUI.Compiler"]
Canonical["canonical IUR"]
LiveView["AshUI.LiveView.Integration"]
Screen --> Authority
Elements --> Authority
Authority --> Persisted
Persisted --> Compiler
Compiler --> Canonical
Canonical --> LiveView
The practical implication is important: the persisted unified_dsl payload is a
runtime snapshot, not the primary authoring API.
Use the source that matches your environment. During local development this often means a path dependency:
# mix.exs
defp deps do
[
{:ash_ui, path: "../ash_ui"},
{:ash, "~> 3.0"},
{:phoenix_live_view, "~> 1.0"}
]
endThen fetch dependencies:
mix deps.getIf you use the shipped Postgres-backed defaults, also configure the default UI storage domain and repo. If you are prototyping or writing tests, ETS-backed authoring resources are a simpler place to start.
The checked-in example suite is now the fastest way to inspect current Ash UI authoring and fallback-renderer behavior without guessing which surfaces are stable.
Start at examples/README.md, then use the root workflow:
mix ash_ui.examples.list
mix ash_ui.examples.preview button
mix ash_ui.examples.start dialog --dry-runRepresentative directories by family:
- foundational content:
text,button,label,icon,image - forms and inputs:
form_builder,field_group,text_input,select,toggle - layout, navigation, and display:
row,grid,menu,viewport,canvas - overlays, data, feedback, and operations:
dialog,table,progress,cluster_dashboard
Use the example suite as the honest runtime surface. Directory names stay in
parity with the sibling unified_ui catalog, but some Ash UI examples are
normalized or explicitly custom:* where the current public widget vocabulary
is narrower.
The minimal shape is:
- One screen resource using
AshUI.Resource.DSL.Screen - One or more element resources using
AshUI.Resource.DSL.Element - Ash relationships plus
ui_relationshipsto define composition order
defmodule MyApp.UI.Domain do
use Ash.Domain, validate_config_inclusion?: false
resources do
resource MyApp.UI.WelcomeScreen
resource MyApp.UI.WelcomeHero
resource MyApp.UI.RefreshButton
end
end
defmodule MyApp.UI.ElementBase do
defmacro __using__(_opts) do
quote do
use Ash.Resource, domain: MyApp.UI.Domain, data_layer: Ash.DataLayer.Ets
use AshUI.Resource.DSL.Element
ets do
private?(true)
end
attributes do
uuid_primary_key(:id)
attribute(:screen_id, :uuid, allow_nil?: true)
attribute(:parent_id, :uuid, allow_nil?: true)
end
actions do
defaults([:read])
end
end
end
end
defmodule MyApp.UI.WelcomeHero do
use MyApp.UI.ElementBase
ui_element do
type :hero
props %{
eyebrow: "AshUI",
title: "Screens are Ash resources",
message: "Author resources first, then persist and mount them."
}
metadata %{id: "welcome_hero"}
end
end
defmodule MyApp.UI.RefreshButton do
use MyApp.UI.ElementBase
ui_element do
type :button
props %{label: "Refresh", variant: "primary"}
metadata %{id: "refresh_button"}
end
ui_actions do
action :refresh_dashboard do
signal :click
target "refresh"
source %{resource: "Demo.Dashboard", action: "refresh", id: "dashboard-1"}
transform %{}
end
end
end
defmodule MyApp.UI.WelcomeScreen do
use Ash.Resource, domain: MyApp.UI.Domain, data_layer: Ash.DataLayer.Ets
use AshUI.Resource.DSL.Screen
ets do
private?(true)
end
attributes do
uuid_primary_key(:id)
end
actions do
defaults([:read])
end
relationships do
has_many :hero_elements, MyApp.UI.WelcomeHero do
destination_attribute(:screen_id)
end
has_many :action_buttons, MyApp.UI.RefreshButton do
destination_attribute(:screen_id)
end
end
ui_relationships do
relationship :hero_elements do
kind :child
slot :body
placement :append
order 0
end
relationship :action_buttons do
kind :companion
slot :actions
placement :append
order 1
end
end
ui_screen do
layout :column
route "/welcome"
metadata %{title: "Welcome"}
end
endRuntime loading works from the configured Screen storage resource, so the
usual next step is to persist the resource-authority snapshot:
alias AshUI.Resource.Authority
{:ok, _screen_record} =
Authority.create(MyApp.UI.WelcomeScreen,
name: "welcome",
route: "/welcome",
metadata: %{title: "Welcome"}
)Authority.create/2 stores the compiled authoring snapshot into the configured
screen resource. Later compiler/runtime calls use that persisted record as the
load root.
defmodule MyAppWeb.WelcomeLive do
use MyAppWeb, :live_view
alias AshUI.LiveView.Integration
def mount(_params, _session, socket) do
socket =
assign(socket, :current_user, %{id: "admin-1", role: :admin, active: true})
Integration.mount_ui_screen(socket, :welcome, %{})
end
endOn mount, AshUI currently:
- Reads
:current_userfrom the socket. - Loads the persisted screen by id or name.
- Authorizes the mount.
- Compiles the screen into internal IUR and canonical IUR.
- Evaluates bindings.
- Assigns screen state back onto the socket.
After this first screen works:
- Read UG-0002 for the full resource-local DSL.
- Read UG-0003 before designing a larger widget library.
- Read UG-0004 when you need interactive screens.
- Review examples/README.md when you want a current, runnable example for a specific widget family.