What the agent does
The agent starts with the router, because in a Phoenix app with a catch-all live route the place a health route is declared decides whether it works. In an app like fly-apps/live_beats, lib/live_beats_web/router.ex defines a :browser pipeline and an :api pipeline that accepts JSON but is used by no scope, and its main scope declares live "/:profile_username", ProfileLive, :show inside an :authenticated live session. A GET /health added after that scope would match the profile route and redirect to sign-in. mix.exs pins Phoenix ~> 1.7.1, ecto_sql ~> 3.11 and postgrex, and mix.lock resolves Phoenix to 1.7.12 and Plug to 1.16.0. lib/live_beats_web/endpoint.ex already runs Plug.RequestId and Plug.Telemetry.
It then reads what the deployment checks today and what the app depends on. fly.toml has a [[services.tcp_checks]] block on internal port 4000 every fifteen seconds with a twenty-second grace period, so the platform only knows the port is open, not that the app can reach Postgres. config/runtime.exs requires DATABASE_URL in production and sets the pool to POOL_SIZE or five, and lib/live_beats/application.ex starts LiveBeats.Repo, Phoenix.PubSub, Presence, Finch, a FLAME.Pool and the endpoint under one supervisor. lib/live_beats/repo.ex is a plain Postgres repo, so a readiness check is a Repo.query("SELECT 1") with a short timeout, nothing more.
From that it writes lib/live_beats_web/controllers/health_controller.ex with two actions. live answers 200 with {"status": "ok"} and touches nothing, so it tells an orchestrator only that the BEAM is serving requests. ready runs the SELECT 1 through the repo with a timeout well under the check interval and answers 200 with {"status": "ok", "checks": {"database": "ok"}} or 503 with the same shape and "error" when the query fails or times out, catching the DBConnection errors rather than letting them become a 500. The router gets a scope "/health" that pipes through :api and is declared before the browser scopes so the paths never reach the profile route. fly.toml gains a [[services.http_checks]] block on /health/ready with the existing interval, timeout and grace period, and the pull request explains that a readiness failure then takes the instance out of routing while the database is unreachable.
Then it writes test/live_beats_web/controllers/health_controller_test.exs on LiveBeatsWeb.ConnCase, the same case redirect_controller_test.exs uses: one test that GET /health/live returns the JSON with status 200, one that GET /health/ready returns 200 with the database check ok against the sandboxed test repo, and one that makes the check fail by pointing the controller's query function, which it reads from application config, at one that raises, and asserts the 503 body. It starts Postgres in the sandbox to match config/test.exs, runs mix test, which the alias in mix.exs chains behind ecto.create and ecto.migrate, runs mix format --check-formatted, and fixes anything until both pass.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Add health check endpoints to this Phoenix app. Read mix.exs, mix.lock, lib/live_beats_web/router.ex, lib/live_beats_web/endpoint.ex, lib/live_beats/application.ex, lib/live_beats/repo.ex, config/runtime.exs, config/test.exs, fly.toml, test/support/conn_case.ex and test/live_beats_web/controllers/redirect_controller_test.exs first. Add a HealthController with a live action that returns 200 and JSON without touching any dependency, and a ready action that runs SELECT 1 through the repo with a short timeout and returns 200 with a checks map, or 503 with the same shape when the query fails or times out, catching the database errors instead of raising. Mount them under /health through the api pipeline, declared before any scope with a catch-all live route. Replace the tcp check in fly.toml with an http check on the readiness path using the existing interval, timeout and grace period. Write ExUnit tests on ConnCase for both actions, including one that forces the database check to fail. Start Postgres in the sandbox, run mix test and mix format --check-formatted, fix anything until they pass, and open a pull request that explains what each endpoint means and how the platform check was changed.",
"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 its own branch with the new controller, the /health scope in the router, the fly.toml change and the test file. The description says what each endpoint means to an orchestrator, why the scope is declared where it is, what the readiness timeout is and why it is shorter than the check interval, and that a readiness failure now removes the instance from routing where the TCP check kept it in.
The transcript on the session keeps every command with its output: the Postgres start, mix deps.get, the mix test run with the new tests, the formatter check, and the curl calls against a running mix phx.server showing the two JSON bodies and the 503 when Postgres is stopped. There is no CI workflow in this repository, so that transcript is the test record for the change.
Before merging, a reviewer checks that the readiness query timeout fits the pool settings in config/runtime.exs, that the platform check points at readiness and not liveness, that nothing in the readiness path needs authentication or a session, and that the two routes are reachable in a release build where Mix.env() guards are gone. One follow-up message to the same session adds a check for another dependency, such as the uploads mount under BUCKET_MOUNT, to the same checks map.
Run it on every Linear issue labelled observability
A team that tracks operational work in Linear starts one run per issue from an automation with a linear trigger on the observability label. The automation adds the issue after this prompt, so the agent reads which dependencies the issue wants checked and starts in the repository the automation names.
Each run opens its own pull request from its own sandbox, and an automation run has its own max_runs_per_hour cap. The pull request links back to the issue, and the transcript holds the test run and the live requests for that change.
The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.
Why run it on Rush?
The code is short; the checking is not. Proving a readiness endpoint means starting Postgres, running the app, stopping Postgres and watching the endpoint answer 503 instead of hanging or crashing the request, then running the suite with the sandbox adapter. On a laptop that competes with the database already running for development. In a Rush sandbox the agent has its own Postgres and its own port 4000, and the sandbox parks when the turn ends.
The pull request carries its own proof. The JSON from both endpoints, the 503 with the database down and the green mix test run are in the transcript beside the commands that produced them, so a reviewer can see the endpoint behaves under the failure it exists to report.
Frequently asked questions
Why two endpoints instead of one /health?
Because they answer different questions. Liveness says the VM is serving requests and should never fail because a dependency is down, or an orchestrator would restart a healthy process. Readiness says the app can do useful work right now, which for this app means reaching Postgres, and a failure there should pull the instance out of routing until the database is back.
Why is the route declared before the browser scopes?
Because live "/:profile_username" matches any single-segment path and sits in an authenticated live session, so a /health route declared after it would redirect to sign-in. Phoenix matches routes in declaration order, so the /health scope goes first and pipes through :api, which skips sessions, CSRF and layouts.
Will the health checks fill the logs?
Phoenix logs each request at info by default, so a check every fifteen seconds adds a few lines a minute. The pull request leaves logging as it is and notes the router and endpoint options that lower the level for these routes if the team wants them quiet.
Does it change the Fly deployment?
Only the check block in fly.toml: the TCP check becomes an HTTP check on /health/ready with the same interval, timeout and grace period. The release command, the service ports and the concurrency limits are untouched, and the pull request describes the behavioural difference so the team can keep the TCP check instead if they prefer.