Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
cast_assoc/3 for has_many/has_one — never manually insert children in a separate stepEcto.Multi for operations spanning multiple unrelated tables — do NOT use Ecto.Multi for nested associationson_delete explicitly in migrations — :delete_all for owned children, :nothing for independent entitieson_replace: :delete in cast_assoc for list managementcast_assoc compares against currently loaded datacast_assoc sets them automaticallyRepo.transaction/1 with Ecto.Multi — wrap multi-table operations for atomicityhas_many/belongs_to with appropriate on_replace strategyon_delete and create indexcast_assocRepo.insert/Repo.update with parent changeset{:ok, _} and {:error, changeset}defmodule MyApp.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
has_many :comments, MyApp.Blog.Comment
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title])
|> validate_required([:title])
|> cast_assoc(:comments, with: &MyApp.Blog.Comment.changeset/2)
end
end
defmodule MyApp.Blog.Comment do
use Ecto.Schema
import Ecto.Changeset
schema "comments" do
field :body, :string
belongs_to :post, MyApp.Blog.Post
timestamps()
end
def changeset(comment, attrs) do
comment
|> cast(attrs, [:body])
|> validate_required([:body])
end
end
# Usage — create post with comments in one operation
Blog.create_post(%{
title: "My Post",
comments: [
%{body: "First comment"},
%{body: "Second comment"}
]
})case Repo.insert(Post.changeset(%Post{}, attrs)) do
{:ok, post} ->
{:ok, post}
{:error, changeset} ->
# Top-level errors on changeset.errors
# Nested errors on changeset.changes[:comments] (list of changesets)
{:error, changeset}
enddefmodule MyApp.Recipes.Recipe do
schema "recipes" do
field :name, :string
has_many :ingredients, MyApp.Recipes.Ingredient, on_replace: :delete
timestamps()
end
def changeset(recipe, attrs) do
recipe
|> cast(attrs, [:name])
|> validate_required([:name])
|> cast_assoc(:ingredients, with: &MyApp.Recipes.Ingredient.changeset/2)
end
end
# Update — send the full list; omitted items are deleted
def update_recipe(recipe, attrs) do
recipe
|> Repo.preload(:ingredients)
|> Recipe.changeset(attrs)
|> Repo.update()
enddef create_order_with_payment(order_attrs, payment_attrs) do
Ecto.Multi.new()
|> Ecto.Multi.insert(:order, Order.changeset(%Order{}, order_attrs))
|> Ecto.Multi.insert(:payment, fn %{order: order} ->
Payment.changeset(%Payment{}, Map.put(payment_attrs, :order_id, order.id))
end)
|> Repo.transaction()
endcase create_order_with_payment(order_attrs, payment_attrs) do
{:ok, %{order: order, payment: payment}} ->
{:ok, order}
{:error, failed_operation, failed_changeset, _changes_so_far} ->
Logger.error("Multi failed at #{failed_operation}: #{inspect(failed_changeset.errors)}")
{:error, failed_changeset}
enddefmodule MyApp.Repo.Migrations.CreateComments do
use Ecto.Migration
def change do
create table(:comments) do
add :body, :text
add :post_id, references(:posts, on_delete: :delete_all)
timestamps()
end
create index(:comments, [:post_id])
end
endConfirm FK indexes exist in psql with \d comments. Expect an entry such as comments_post_id_index. If missing, add it in a new migration:
def change do
create index(:comments, [:post_id])
endUse a join schema with cast_assoc for full control over nested creation and updates:
# Schema
schema "posts" do
field :title, :string
many_to_many :tags, MyApp.Blog.Tag, join_through: MyApp.Blog.PostTag, on_replace: :delete
timestamps()
end
# Join schema
defmodule MyApp.Blog.PostTag do
use Ecto.Schema
schema "post_tags" do
belongs_to :post, MyApp.Blog.Post
belongs_to :tag, MyApp.Blog.Tag
timestamps()
end
end
# Parent changeset — use cast_assoc with the join schema
def changeset(post, attrs) do
post
|> cast(attrs, [:title])
|> validate_required([:title])
|> cast_assoc(:post_tags, with: &PostTag.changeset/2)
endWhen updating a nested association with only some fields, preload the association first and rely on Ecto's internal ID matching — do not require :id in the child changeset:
def update_post(post, %{post: post_attrs, comments: comments_attrs}) do
post
|> Repo.preload(:comments)
|> Post.changeset(%{post_attrs | comments: comments_attrs})
|> Repo.update()
end| ❌ Don't | ✅ Do |
|---|---|
| Manually insert children in a separate step | Use cast_assoc/3 for has_many/has_one |
Use Ecto.Multi for nested associations | Use cast_assoc; reserve Ecto.Multi for unrelated tables |
| Build the update changeset without preloading | Repo.preload/2 before cast_assoc compares data |
| Require the FK in the child changeset | Let cast_assoc set the FK automatically |
Omit on_replace on a managed list | Set on_replace: :delete to remove omitted items |
Leave on_delete unset in the migration | :delete_all for owned children, :nothing for independent |
| Forget the foreign key index | create index(:comments, [:post_id]) |
| Predecessor | This Skill | Successor |
|---|---|---|
| ecto-changeset-patterns | ecto-nested-associations | testing-essentials |
| ecto-essentials | ecto-nested-associations | apply-ecto-conventions |
Companion skills:
ecto-essentials — schema, migration, and association basicsecto-changeset-patterns — changeset composition and cast_assoc rulesecto-migration — migration planning for FK and on_delete changestesting-essentials — testing nested creates, updates, and error cases.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators