Elixir/Phoenix Recipes
Phoenix recipes for AI coding agents are a project setup and a set of prompts that make Claude Code, Codex and Cursor ship trustworthy Phoenix 1.8 changes: the generated AGENTS.md and mix precommit alias as the definition of done, Tidewave for runtime access, tests written before the code, and CI gates that fail on compiler warnings and security findings.
This page is for Elixir developers who let an agent write most of a feature, and for tech leads who want the same gates enforced for the whole team (see the CI job and the sign-off table). The situation it fixes: the agent’s pull request is green, but the LiveView keeps every order in socket assigns, the test that “proves” the GenServer works uses Process.sleep(100), the payment call sits inside Repo.transaction, and a failing assertion was “fixed” by changing the expected value. Phoenix already gives you an agent rules file, a precommit alias and generators. The recipes below make the agent run into them on every turn, so you review evidence instead of diffs.
What you’ll walk away with from these Elixir recipes
Section titled “What you’ll walk away with from these Elixir recipes”- A Phoenix project whose
AGENTS.md,mix precommitalias and dependency rules keep the agent on current Phoenix idioms, wired into all three tools. - Tidewave MCP set up so the agent evaluates code in your running app, reads its logs and queries the dev database instead of guessing.
- A Claude Code
Stophook that refuses “done” untilmix precommitpasses, and a CI job that is the mechanical gate for Codex and Cursor. - Eight copy-paste prompts: an
AGENTS.mddraft, a test-first context and JSON API (tests, then code), a LiveView with streams, an ETS rate limiter, an Ecto.Multi order transaction, an Oban worker and a Docker release. - A sign-off table that says which agent changes a person reviews and which the gates prove.
Which Elixir, Phoenix and Hex versions do these recipes assume?
Section titled “Which Elixir, Phoenix and Hex versions do these recipes assume?”Agents mix APIs from Phoenix 1.5 to 1.8 and LiveView 0.x to 1.x because their training data does. Pin the versions (.tool-versions or mise.toml, and mix.lock) and name them in AGENTS.md. These were current on 2026-09-26:
| Component | Version | Source (checked 2026-09-26) |
|---|---|---|
| Elixir / Erlang/OTP | 1.20.4 / 29.1.1 | hexpm/elixir image tags on Docker Hub |
phoenix / phx_new | 1.8.15 | hex.pm |
phoenix_live_view | 1.2.12 | hex.pm |
ecto / ecto_sql | 3.14.2 / 3.14.0 | hex.pm |
oban / oban_web | 2.24.1 / 2.13.0 | hex.pm |
credo / sobelow / mix_audit | 1.7.19 / 0.15.0 / 2.1.5 | hex.pm |
stream_data | 1.4.0 | hex.pm |
tidewave / usage_rules | 0.9.1 / 1.2.8 | hex.pm |
The Phoenix behaviour described here (the generated AGENTS.md, the precommit alias, phx.gen.release) was read from the phx_new 1.8.15 installer source. Elixir flags come from the v1.20.4 Mix task docs. The hooks and CI steps follow each tool’s documentation; run them on your project before you rely on them.
How do you make a Phoenix project agent-ready?
Section titled “How do you make a Phoenix project agent-ready?”Start from what mix phx.new already generates. In Phoenix 1.8.15 it writes an AGENTS.md (skip it with --no-agents-md) that combines project guidelines with rules for Elixir, Phoenix, Ecto, HEEx and LiveView. Among them: use LiveView streams for collections, avoid LiveComponents without a specific need, start processes in tests with start_supervised!/1, never use Process.sleep/1 in tests, and never call String.to_atom/1 on user input. It also adds one alias that is the natural definition of done:
def cli do [preferred_envs: [precommit: :test]]end
defp aliases do [ # ... precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"] ]endmix precommit compiles with warnings as errors, drops unused lock entries, formats and runs the suite. The first step matters more on Elixir 1.20: its type checker infers types across clauses and reports typing violations and dead code as compiler warnings, so the alias fails on a class of bug no test was written for. Add the checks an agent most often skips, and add their packages to deps with only: [:dev, :test], runtime: false:
precommit: [ "compile --warnings-as-errors", "deps.unlock --unused", "format", "credo --strict", "sobelow --exit Low --skip", "test"]For an app created before Phoenix 1.8, generate the same rules from your dependencies with usage_rules: add {:usage_rules, "~> 1.2", only: [:dev]}, set usage_rules: [file: "AGENTS.md", usage_rules: [:elixir, :otp, "phoenix:ecto", "phoenix:liveview"]] in project/0, and run mix usage_rules.sync. The sync re-writes its own section on every run, so rules follow mix.lock when a dependency ships new ones.
Then add your project’s own lines to AGENTS.md, below the generated block:
## Commands- One test or file: `mix test test/shop/orders_test.exs:42`- Failures from the last run: `mix test --failed`- Full gate: `mix precommit` (compile, format, Credo, Sobelow, tests)
## Definition of done`mix precommit` passes. Report the command you ran and its last lines.
## Rules- Generate contexts, schemas and migrations with `mix phx.gen.*` and `mix ecto.gen.migration`, never by hand.- Tests are the specification. If a test fails, fix lib/, not test/. Ask before changing an assertion.- Never call an external service inside `Repo.transaction` or `Ecto.Multi`.- Money is integer cents or `Decimal`, never float.Each tool loads the file differently:
Create a CLAUDE.md whose first line is @AGENTS.md, then add any Claude-only lines below it. Claude Code 2.1.277 and later also read AGENTS.md directly when a project has no CLAUDE.md (on the latest release channel on 2026-09-26); the import works on every version and channel.
Codex reads AGENTS.md before it starts work. Since codex-cli 0.150.0, a project Codex does not trust does not supply its AGENTS.md, so trust the repository when Codex asks.
Add an always-applied project rule (the format is on Cursor project rules) that repeats the Commands, Definition of done and Rules sections, plus the generated LiveView and Ecto rules you rely on. Commit the rule with the repository.
Shared agent rules explains how to keep one source of truth when a team uses all three tools.
How do you give the agent your running Phoenix app?
Section titled “How do you give the agent your running Phoenix app?”An agent that can only read files guesses what Orders.list_orders/1 returns and what the last request logged. Tidewave is an open-source MCP server from Dashbit that runs inside your Phoenix app in development. It exposes project_eval (run Elixir code in the running app), execute_sql_query (against the dev database), get_logs, get_source_location and get_docs, which reads documentation for the exact versions in mix.lock.
Install it with mix igniter.install tidewave (after mix archive.install hex igniter_new), or add {:tidewave, "~> 0.9", only: :dev} and put plug Tidewave inside an if Mix.env() == :dev do block right above if code_reloading? do in lib/my_app_web/endpoint.ex. Start the server with mix phx.server, then connect your tool (port 4000 is the Phoenix default):
claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcpCheck it with /mcp in a session. Tidewave 0.9.1 has no authentication (it accepts only localhost connections by default), so do not pick Authenticate.
codex mcp add tidewave --url http://localhost:4000/tidewave/mcpcodex mcp list shows the entry but does not test the connection; run /mcp in the TUI to see the tools.
{ "mcpServers": { "tidewave": { "url": "http://localhost:4000/tidewave/mcp" } }}Start a new chat and ask the agent to list Tidewave’s tools.
The before and after is concrete. Without Tidewave, the agent reads a stack trace, edits a changeset and re-runs the suite. With it, the agent calls Shop.Orders.create_order/2 with the failing input in the running app, reads the resulting log line and the orders row, and edits once. Tidewave accepts only local connections by default and must never run in production.
How do you stop the agent from declaring done before the tests pass?
Section titled “How do you stop the agent from declaring done before the tests pass?”An instruction in AGENTS.md is advice. A hook or a CI check is a gate. Claude Code gets a local gate from a Stop hook. The Codex and Cursor setups below are still instructions, so for them the CI job after the tabs is the only mechanical gate.
A Stop hook runs when Claude finishes responding. Exit code 2 blocks the stop and shows the hook’s stderr to Claude, which keeps working:
#!/usr/bin/env bash# Refuse "done" until mix precommit passes.input=$(cat)# Already continuing because of this hook: stop and report instead of looping.[ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ] && exit 0cd "$CLAUDE_PROJECT_DIR" || exit 0# Nothing under lib, test, config or priv changed: nothing to check.[ -z "$(git status --porcelain -- lib test config priv mix.exs mix.lock)" ] && exit 0if ! out=$(mix precommit 2>&1); then echo "mix precommit failed. Fix lib/, never test/. Re-run failures with mix test --failed:" >&2 echo "$out" | tail -40 >&2 exit 2fiexit 0Register it in .claude/settings.json under hooks → Stop as a command handler pointing at ${CLAUDE_PROJECT_DIR}/.claude/hooks/elixir-gate.sh with "timeout": 600, and make the script executable. The script needs jq. Because it exits 0 when stop_hook_active is true, a failing gate forces one retry and then Claude reports instead of looping on a test that cannot pass, for example when PostgreSQL is down. mix precommit runs format, so the hook also rewrites unformatted files. Hooks automation shows the full settings block and how to allow more retries.
Open the repository once in interactive codex and trust it before you run codex exec, or the run will not load AGENTS.md. Fetch dependencies outside the agent (Hex needs the network), then run the loop:
mix deps.getcodex exec --approve-for-me "Implement Shop.Orders so every test in test/shop/orders_test.exs passes. Do not modify anything under test/. Finish only when 'mix precommit' passes; print its last lines."--approve-for-me runs the session in the workspace-write sandbox and routes approval requests, such as a command that needs to reach the test database outside the sandbox, through Codex’s automatic review instead of waiting for you. The prompt is still an instruction: nothing stops the run from ending red. The CI job below is the mechanical gate for Codex.
Paste the implementation prompt into the agent, in Plan Mode first if the change touches a context, a schema and a migration together. Put the Definition of done in the project rule so the agent runs mix precommit before it reports back. The rule is advice too. Cursor’s mechanical gate runs after the pull request opens: the CI job below, plus Bugbot if your team uses it for review.
CI decides, whatever ran locally. Run the same checks with a read-only token and a throwaway PostgreSQL service:
name: elixir-gateson: pull_request: types: [opened, synchronize, reopened, labeled, unlabeled]permissions: contents: readjobs: gates: runs-on: ubuntu-latest services: postgres: image: postgres:17 env: POSTGRES_HOST_AUTH_METHOD: trust # disposable CI database, no credential ports: ["5432:5432"] options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=5 env: MIX_ENV: test steps: - uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 2 # the PR merge commit and its base parent - name: Existing tests unchanged if: ${{ !contains(github.event.pull_request.labels.*.name, 'test-change-approved') }} run: | changed=$(git diff --name-only --diff-filter=MDR HEAD^1 HEAD -- test/) if [ -n "$changed" ]; then echo "Existing tests modified, moved or deleted; a reviewer must add the test-change-approved label:" echo "$changed" exit 1 fi - uses: erlef/setup-beam@v1 with: otp-version: "29.1.1" elixir-version: "1.20.4" - uses: actions/cache@v6 with: path: | deps _build key: mix-${{ runner.os }}-${{ hashFiles('mix.lock') }} - run: mix deps.get - run: mix deps.unlock --check-unused - run: mix hex.audit # retired packages - run: mix deps.audit # known advisories (mix_audit) - run: mix format --check-formatted - run: mix compile --warnings-as-errors - run: mix credo --strict - run: mix sobelow --exit Low --skip - run: mix ecto.create && mix ecto.migrate && mix ecto.rollback --step 1 && mix ecto.migrate - run: mix test --warnings-as-errorsThe “Existing tests unchanged” step fails when the pull request modifies, renames or deletes a file that already exists under test/; new test files pass. After a person reviews the test change, they add the test-change-approved label, which starts a new run that skips the step (re-running the old job does not work, because it reuses the original event). The step is only a gate if the pull request cannot remove it: GitHub runs the workflow file from the pull request itself, so put /.github/ under CODEOWNERS and require that review in branch protection or a ruleset. The rollback step proves the newest migration is reversible. Generated Phoenix apps use the postgres user by default, which the trust service accepts.
Which Phoenix recipes should you hand to the agent?
Section titled “Which Phoenix recipes should you hand to the agent?”Every prompt below assumes the AGENTS.md and gate above. Each one names the tests first, because a test the agent wrote before the code is the cheapest oracle you have.
Build a context and JSON API test-first
Section titled “Build a context and JSON API test-first”Ask for the generator command and failing tests only, check that the new tests fail for the right reason, then ask for the implementation in a second prompt. Splitting the turns is what stops the agent from writing tests that match its own code.
Keep the approval step in the first prompt: mix phx.gen.json writes a context, schema, migration, controller and tests at once, and approving one command is cheaper than reviewing the files it produces.
Build a LiveView that does not grow without bound
Section titled “Build a LiveView that does not grow without bound”Older code keeps lists in assigns or uses temporary_assigns; the rules Phoenix 1.8 generates tell the agent to use streams for collections and to avoid LiveComponents unless there is a specific need. Hold the agent to them.
Write OTP code the tests can trust
Section titled “Write OTP code the tests can trust”The failure here is rarely the GenServer; it is the test. An agent that waits with Process.sleep/1 produces a suite that passes on a laptop and flakes in CI.
Keep external calls out of Ecto transactions
Section titled “Keep external calls out of Ecto transactions”A transaction that calls a payment gateway holds a database connection for the whole network round trip, and it cannot roll back a charge that succeeded. Tell the agent where the boundary is.
Add background jobs with Oban
Section titled “Add background jobs with Oban”Ship a release the agent can prove boots
Section titled “Ship a release the agent can prove boots”mix phx.gen.release --docker generates lib/my_app/release.ex, rel/overlays/bin/migrate, rel/overlays/bin/server, a Dockerfile and a .dockerignore. The Dockerfile builds from a hexpm/elixir Debian image pinned to the Elixir and OTP versions you generated it with. Let the generator write the files and ask the agent to prove they work. Container hardening beyond that is on Docker patterns.
How do you know the agent’s tests would catch a bug?
Section titled “How do you know the agent’s tests would catch a bug?”Green tests the agent wrote prove only that the code agrees with the agent. Three cheap checks raise the bar without you reading every line:
- Property tests on changesets and pure functions. Add
{:stream_data, "~> 1.4", only: :test}and ask: “For Task.changeset/2, write ExUnitProperties properties: any title shorter than 3 characters is invalid; any valid attrs round-trip through create_task/1 and get_task!/1; no status transition out of :done except to :cancelled is valid.” Properties generate the inputs the agent did not think of. Property-based testing covers the method. - Break it on purpose. Ask the agent in a separate session to introduce one bug per context function (flip a comparison, drop a
where, skip a validation) and run the suite after each. Every bug the suite does not catch is a missing test; keep the tests, discard the bugs. - Flaky-test hunt.
mix test --repeat-until-failure 50 test/shop/rate_limiter_test.exs(Elixir 1.17+) reruns a file until it fails, which exposes theProcess.sleepand shared-state races the agent left behind.
Then decide what a person still reviews:
| Change | Proven by | Human sign-off |
|---|---|---|
| Context functions, changesets, controllers, LiveViews | Tests written before the code, mix precommit, CI | Not needed when the gates pass and existing tests are unchanged |
| Modified existing tests | The test-change-approved label | Yes: the person who adds the label |
| Migrations that drop, rename or backfill | The rollback step in CI | Yes: check lock time and data loss (see migration patterns) |
| Supervision tree and restart strategy | Tests that kill a child and assert siblings survive | Yes: a wrong strategy fails only under load |
Authentication, authorisation, raw/1 in templates | Sobelow, tests for denied paths | Yes |
Attach the mix precommit output and the CI run to the pull request as its evidence bundle, so the reviewer checks proof, not code.