What the agent does
The agent reads mix.exs and mix.lock to see what can move and what is held on purpose. In hexpm/hexpm the lock has 158 entries. Most requirements are loose: {:phoenix, "~> 1.6"} resolves to 1.8.15, phoenix_live_view ~> 1.0 to 1.2.12, ecto and ecto_sql ~> 3.0 to 3.14.2 and 3.14.0, oban ~> 2.23 to 2.24.1, bandit ~> 1.0 to 1.12.5, sentry ~> 13.0 to 13.5.1. Four dependencies are held with a comment that says why: finch comes from a GitHub ref with override: true because a fix for idle connections closing inside the pool has no release yet (sneako/finch#392); hex_core comes from GitHub with a TODO to return to ~> 0.19 once a release ships the per-repository policy model; git_diff is a GitHub ref; and {:req, "0.7.5"} is an exact pin because ExAws signs empty-body GETs that Req 0.7 rewrites as POST (ex-aws/ex_aws#1246). ssl_verify_fun is forced through rebar3 with an override. And 54 lumis_wasm_* grammar packages are all at ~> 0.26.0 beside lumis ~> 0.10 and mdex ~> 0.14, so they move as one family or not at all.
Then it reads what a green run proves. .github/workflows/main.yml runs on OTP 29.1 and Elixir 1.20.4 with a postgres:18 service: a format job that does mix deps.get, mix compile --warnings-as-errors, mix format --check-formatted and mix ecto.setup to verify the seeds, a test-hexpm job that runs mix test with MIX_ENV=test, and a test-hex job that clones the hex client at main, v2.5 and v2.4 and runs its suite against this app with --include integration. The test alias in mix.exs runs ecto.create --quiet, ecto.load --skip-if-loaded and ecto.migrate first. config/config.exs pins the asset tools separately, esbuild 0.25.0 and tailwind 4.1.11, and the Dockerfile builds with the same Elixir and Erlang and runs mix sentry.package_source_code + release. .github/dependabot.yml already groups weekly mix updates with a seven-day cooldown, so the agent's job is the part Dependabot cannot do: run the suite per group, port code where a release needs it, and leave the explained pins alone.
The agent starts Postgres in its sandbox, runs mix deps.get and mix hex.outdated to see which packages have a release beyond the lock, and then updates in groups with mix deps.update <names>, never --all: the Phoenix family (phoenix, phoenix_live_view, phoenix_html, phoenix_ecto, phoenix_view, phoenix_live_dashboard, plug, bandit) together, because a LiveView release and a Phoenix release often expect each other; the Ecto family (ecto, ecto_sql, postgrex, ecto_psql_extras); oban on its own, since its migrations are versioned; the lumis, mdex and lumis_wasm_* family as one group; the test-only packages mox, stream_data, lasso and lazy_html together; and each remaining package alone. After each group it runs mix compile --warnings-as-errors, mix format --check-formatted and mix test, and keeps the group as one commit only if all three pass; a failing group is reverted with the first compiler warning or test failure kept.
The held dependencies are reported rather than moved. For each GitHub ref and for the req pin the agent checks whether the upstream issue named in the comment has a release that includes the fix, and lists the answer in the pull request: a dependency it could return to Hex, with the version, or one that still has to wait. It does not change config/config.exs tool versions or the Dockerfile unless a group requires a newer Elixir, which it would report instead of doing.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "codex",
"input": "Upgrade this Phoenix app's Hex dependencies. Read mix.exs, mix.lock and .github/workflows/main.yml first; in an app shaped like hexpm/hexpm, use the OTP and Elixir versions CI pins, start Postgres in the sandbox and set MIX_ENV=test. Run mix deps.get and mix hex.outdated, then update in groups with mix deps.update naming the packages, never --all: the Phoenix family (phoenix, phoenix_live_view, phoenix_html, phoenix_ecto, phoenix_view, phoenix_live_dashboard, plug, bandit), the Ecto family (ecto, ecto_sql, postgrex, ecto_psql_extras), oban alone, the lumis, mdex and lumis_wasm_* grammar family as one group, the test-only packages together, and each remaining package on its own. After each group run mix compile --warnings-as-errors, mix format --check-formatted and mix test; keep the group as one commit only if all pass, otherwise revert it and record the first error. Leave every dependency whose mix.exs entry carries a comment explaining a pin or a GitHub ref unchanged, and for each one check whether the upstream issue it names now has a release with the fix and report the answer. Do not change the esbuild or tailwind versions in config or the Dockerfile. Open one pull request with one line per bump with old and new versions, each skipped package with its error, and the status of every held dependency.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a pull request on a new branch with one commit per group that passed, each changing mix.lock and, where a requirement had to be raised to admit the new version, the one line in mix.exs. The commented pins keep their lines, config/config.exs and the Dockerfile do not change, and any code port a group needed, such as a renamed function in a Phoenix component, sits in that group's commit.
The description has one line per bump with old and new versions, the groups reverted with the first warning or failing test, a short table of the held dependencies with whether their upstream fix has shipped, and the mix compile --warnings-as-errors, mix format and mix test results for the final tree. The transcript keeps the full output of every group, including the reverted ones.
Before merging, a reviewer looks at the Phoenix group first, since --warnings-as-errors turns a new deprecation into a failed build, then checks that an oban bump did not need a migration the commit lacks, and reads the held-dependency table to decide whether to drop a pin in a separate pull request.
Run it on every Monday morning
Create an automation in the Project with this prompt and a schedule trigger (cron 0 9 * * 1 in your timezone). Each week's run opens one pull request with that week's bumps, or none when mix hex.outdated shows nothing new; the held dependencies are re-checked every time, so the week an upstream fix ships the table says so. Because each group is its own commit, dropping a bump before merge is a revert of one commit.
If Dependabot already opens a grouped weekly pull request, as .github/dependabot.yml does here, point the automation's prompt at that branch instead: the agent runs the suite on Dependabot's bumps, splits out the ones that fail, and reports which pin Dependabot moved that the comments say should stay. Its runs are not tied to an API key, so read the result from the Project's sessions rather than from a key's webhook.
The use case this job follows, with its automation and trigger: Upgrade dependencies weekly.
Why run it on Rush?
A Phoenix app with 158 locked packages, a 54-package grammar family, four deliberate pins and a --warnings-as-errors build cannot be upgraded by bumping everything and reading the red CI: the failure names a test, not the package that caused it. On Rush the agent takes one group at a time in its own sandbox with Postgres running and the CI versions of OTP and Elixir, keeps what passes, reverts what does not with the error attached, and leaves the pins your comments protect.
The record shows every compile, format and test run per group, which is the evidence a maintainer needs when the package that moved is Phoenix itself. The same request runs against every Phoenix app you maintain at once.
Frequently asked questions
Dependabot already does weekly mix updates here. What does this add?
Dependabot bumps and opens; it does not run mix test against Postgres, split a failing package out of the group, or read the comment that says why req is pinned. The agent does those three things and reports the held dependencies, so the pull request you get is green and explains itself.
Will it move the finch, hex_core or git_diff GitHub refs?
No. Each carries a comment naming the upstream fix it waits for, and the agent leaves the line alone. It does check whether a Hex release now includes that fix and says so in the description, so you can retire the ref deliberately.
Why not mix deps.update --all?
Because one failure then hides in a lock diff of a hundred lines. Named groups keep a failing package out of the commit and make the pull request readable: one commit per group, one line per bump.
Does it run the hex client suite too?
Not by default. The test-hex job clones the hex client at three versions and runs its integration suite against this app, which is a long second suite; the agent runs mix test and says the hex client job did not run. Ask for it in the prompt and it clones hex the way CI does.
What if a Phoenix release needs a newer Elixir?
The agent reverts that group and reports the required Elixir version, because changing the CI pins and the Dockerfile build arguments is a decision for a maintainer, not a side effect of a weekly run.