| name | mix-tasks-generators |
| type | atomic |
| tags | ["atomic"] |
| license | MIT |
| description | MANDATORY when creating custom Mix tasks or using Phoenix generators. Invoke before defining custom Mix task modules with argument parsing and subcommands, scaffolding resources with phx.gen.live or phx.gen.context, generating authentication systems with phx.gen.auth, configuring dependencies and aliases in mix.exs, or writing tests for custom tasks. Covers Mix.Tasks module creation, generator patterns, and Mix project configuration. Trigger words: Mix task, custom task, phx.gen, generators, mix.exs, project configuration, phx.gen.live, phx.gen.auth, scaffold, seed data, ecto.setup, mix help, Mix.Project, OptionParser, @shortdoc, preferred_cli_env, alias, mix run, generator, phx.gen.html, phx.gen.context, phx.gen.json, phx.gen.channel.
|
| metadata | {"version":"1.0.0","user-invocable":"true"} |
Mix Tasks & Generators
Canonical FP bar: docs/fcis-engineering-rules.md — Functional Core, Imperative Shell: pure domain modules; side effects at edges. Generators should emit FCIS-shaped code (thin edges, pure helpers).
RULES — Follow these with no exceptions
1. Always call Mix.Task.run("app.start") first — tasks that access the database or Repo need the app started
2. Create custom tasks for project-specific workflows — don't override standard Mix tasks
3. Register preferred_cli_env in mix.exs — set environment for custom tasks (dev/test/prod)
4. Use transactions for data modifications — wrap Repo.insert_all, Repo.delete_all in Repo.transaction()
5. Test custom tasks with Mix.Project.in_project/4 — ensure tasks work correctly in isolation
6. Follow Mix.Tasks.Namespace.TaskName naming — file path must match: lib/mix/tasks/namespace.task_name.ex
End-to-End Workflow
Follow this sequence when creating a custom Mix task:
- Create file — create
lib/mix/tasks/<namespace>.<task_name>.ex
- Define module — use
defmodule Mix.Tasks.MyApp.TaskName do use Mix.Task
- Add
@shortdoc — one-line description for mix help
- Implement
run/1 — parse args with OptionParser.parse/2 if needed
- Start app — call
Mix.Task.run("app.start") if using Repo
- Implement logic — use transactions for data modifications
- Add to mix.exs — register
preferred_cli_env if needed
- Test — use
Mix.Project.in_project/4 test helper
- Verify — run
mix help | grep task_name to confirm registration
Phoenix Generators
mix phx.gen.json Accounts User users email:string name:string --web API
mix phx.gen.live Blog Post posts title:string body:text status:enum:draft:published:archived
mix phx.gen.live Blog Post posts --table existing_posts
mix phx.gen.context Accounts User users email:string password_hash:string
mix phx.gen.channel Room
mix phx.gen.html Blog Post posts title:string body:text
mix phx.gen.auth Accounts User users --web Admin
mix phx.gen.context Blog Post posts --table posts --app MyApp
Custom Mix Tasks
Basic Task with Transaction Safety
# lib/mix/tasks/my_app.seed_data.ex
defmodule Mix.Tasks.MyApp.SeedData do
use Mix.Task
@shortdoc "Seeds the database with sample data"
@impl Mix.Task
def run(_args) do
Mix.Task.run("app.start")
case MyApp.Repo.transaction(fn -> MyApp.Seeds.run() end) do
{:ok, _} ->
count = MyApp.Repo.aggregate(MyApp.User, :count)
Mix.shell().info("Data seeded successfully! (#{count} users in DB)")
{:error, reason} ->
Mix.shell().error("Seeding failed, transaction rolled back: #{inspect(reason)}")
Mix.raise("Seed failed")
end
end
end
Task with Arguments and Validation
Demonstrates OptionParser with required flags, error reporting, dry-run support, and before/after count verification.
defmodule Mix.Tasks.MyApp.ImportUsers do
use Mix.Task
@shortdoc "Imports users from a CSV file"
@impl Mix.Task
def run(args) do
{opts, _rest, errors} =
OptionParser.parse(args,
strict: [file: :string, dry_run: :boolean],
aliases: [f: :file, d: :dry_run]
)
if Keyword.get(opts, :file) == nil do
Mix.shell().error("Missing required --file argument")
Mix.shell().info("\nUsage: mix my_app.import_users --file <path>")
Mix.raise("Invalid arguments")
end
if errors != [] do
Enum.each(errors, fn {key, value} -> Mix.shell().error("Unknown option: #{key} #{value}") end)
Mix.raise("Invalid arguments")
end
Mix.Task.run("app.start")
file = Keyword.get(opts, :file)
dry_run? = Keyword.get(opts, :dry_run, false)
if dry_run?, do: Mix.shell().info("Dry run mode - no changes will be made")
before_count = MyApp.Repo.aggregate(MyApp.User, :count)
case MyApp.Repo.transaction(fn ->
MyApp.UserImporter.import_from_csv(file, dry_run: dry_run?)
end) do
{:ok, {:ok, count}} ->
after_count = MyApp.Repo.aggregate(MyApp.User, :count)
Mix.shell().info("Imported #{count} users (total: #{after_count}, was: #{before_count})")
{:ok, {:error, reason}} ->
Mix.shell().error("Import validation failed: #{reason}")
Mix.raise("Import failed")
{:error, reason} ->
Mix.shell().error("Import failed, transaction rolled back: #{inspect(reason)}")
Mix.raise("Import failed")
end
end
end
# Usage: mix my_app.import_users --file=users.csv --dry-run
Task with Subcommands
defmodule Mix.Tasks.MyApp.Migrate do
use Mix.Task
@shortdoc "Runs database migrations"
@impl Mix.Task
def run(["up"]), do: Mix.Task.run("ecto.migrate")
def run(["down"]), do: Mix.Task.run("ecto.rollback")
def run(["status"]), do: Mix.Task.run("ecto.migrations")
def run(_) do
Mix.shell().info("""
Usage:
mix my_app.migrate up - Run migrations
mix my_app.migrate down - Rollback last migration
mix my_app.migrate status - Show migration status
""")
end
end
Custom Generators
# lib/mix/tasks/my_app.gen.service.ex
defmodule Mix.Tasks.MyApp.Gen.Service do
use Mix.Task
use Mix.Generator
@shortdoc "Generates a service module"
@impl Mix.Task
def run([name]) do
module_name = Macro.camelize(name)
file_path = "lib/my_app/services/#{name}.ex"
create_file(file_path, service_template(module_name: module_name))
Mix.shell().info("Created service: #{file_path}")
end
embed_template(:service, """
defmodule MyApp.Services.<%= @module_name %> do
@moduledoc \"\"\"
Service module for <%= @module_name %> operations.
\"\"\"
def call(params) do
{:ok, params}
end
end
""")
end
# Usage: mix my_app.gen.service send_email
# Creates: lib/my_app/services/send_email.ex
Mix Project Configuration
Aliases
# mix.exs
defp aliases do
[
setup: ["deps.get", "ecto.setup"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.deploy": ["esbuild default --minify", "phx.digest"]
]
end
Registering Custom Tasks
def project do
[
app: :my_app,
version: "0.1.0",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
deps: deps(),
aliases: aliases(),
preferred_cli_env: [
"my_app.seed_data": :dev,
"my_app.import_users": :dev
]
]
end
Testing Custom Tasks
defmodule Mix.Tasks.MyApp.SeedDataTest do
use ExUnit.Case
setup do
MyApp.Repo.delete_all(MyApp.User)
:ok
end
test "seeds data successfully" do
Mix.Project.in_project(:my_app, ".", fn _ ->
Mix.Tasks.MyApp.SeedData.run([])
assert MyApp.Repo.aggregate(MyApp.User, :count) > 0
end)
end
test "seeds expected count" do
Mix.Project.in_project(:my_app, ".", fn _ ->
Mix.Tasks.MyApp.SeedData.run([])
assert MyApp.Repo.aggregate(MyApp.User, :count) == 10
end)
end
end
Common Task Patterns
| Pattern | Key Steps |
|---|
Database Reset (my_app.reset) | Chain ecto.drop → ecto.create → ecto.migrate → seed via Mix.Task.run/1 |
Health Check (my_app.health_check) | app.start → run named checks (DB, cache, HTTP) → report pass/fail → Mix.raise/1 on any failure |
Cleanup (my_app.cleanup) | Parse --dry-run flag → query expired records → report count → prompt the operator for confirmation → Repo.delete_all/1 only if confirmed |
All follow the same skeleton: parse opts → app.start → transact → report. A task that
deletes data must default to --dry-run reporting and require an explicit --force (or
interactive confirmation) flag before calling Repo.delete_all/1 — never delete
unconditionally based on the task's own judgment.
Common Pitfalls
| ❌ Don't | ✅ Do |
|---|
Access Repo before the app is started | Call Mix.Task.run("app.start") first |
Repo.insert_all / delete_all without a transaction | Wrap data mutations in Repo.transaction/1 |
Override a built-in task like mix test | Namespace custom tasks under Mix.Tasks.MyApp.* |
Mix.Tasks.MyApp.SeedData in a mismatched file | Match the path: lib/mix/tasks/my_app.seed_data.ex |
Omit @shortdoc | Add @shortdoc so the task appears in mix help |
Ignore the OptionParser errors list | Check errors and Mix.raise/1 on invalid args |
| Hand-roll CRUD boilerplate | Scaffold with mix phx.gen.live / phx.gen.context / phx.gen.json |
| Delete records without operator confirmation | Require --force or an interactive y/N prompt before Repo.delete_all/1 |
Integration
| Predecessor | This Skill | Successor |
|---|
| elixir-essentials | mix-tasks-generators | ecto-essentials |
| None (standalone) | mix-tasks-generators | testing-essentials |
Companion skills: ecto-essentials, testing-essentials, oban-essentials.