# Turn Linear Issues Into Pull Requests for a Phoenix App

Yes. Point a run at a Linear issue and your Phoenix repository: the agent changes the stats API, runs mix test on Postgres and ClickHouse and opens a linked PR.

[Run this on Rush](https://cloud.getrush.ai/run/linear-issue-to-pr/phoenix/), in a Project that has your repository.

## What the agent does

The issue goes in as a resource. `https://linear.app/<workspace>/issue/<ID>` in `resources` puts the issue in the prompt through your Linear connection, beside the repository URL that clones the code, and the agent reads description, comments and acceptance criteria before touching a file.

In a Phoenix application like plausible/analytics, `.tool-versions` pins Erlang 28.5, Elixir 1.20 and Node 24, and `mix.exs` defines the `test` alias as `ecto.create --quiet`, `ecto.migrate`, `test` and `clean_clickhouse`, because the suite needs both PostgreSQL and ClickHouse, which `CONTRIBUTING.md` starts with `make postgres` and `make clickhouse`. `config/test.exs` puts `Plausible.Repo` in the SQL sandbox, gives `Plausible.ClickhouseRepo` a pool of 15, swaps the mailer for `Bamboo.TestAdapter`, puts Oban in manual testing mode and replaces the HTTP client with a Mox mock; `test/test_helper.exs` excludes `:slow`, `:minio` and `:migrations` by default.

The stats API is in `lib/plausible_web/router.ex`: the `/api/v1/stats` scope, with `api_scope` set to `stats:read:*` and `PlausibleWeb.Plugs.AuthorizePublicAPI` in its pipeline, routes `realtime/visitors`, `aggregate`, `breakdown` and `timeseries` to `PlausibleWeb.Api.ExternalStatsController`, and the `/api/v2` scope routes `POST /query` to `ExternalQueryApiController.query`, with its JSON schema served at `/api/docs/query/schema.json`. The dashboard's own endpoints go through the `internal_stats_api` pipeline to `StatsController`. A ticket about a public metric or filter lands in `external_stats_controller.ex` or the query layer under it; a ticket about auth lands in the plug.

Tests mirror that: `test/plausible_web/controllers/api/external_stats_controller/` holds `aggregate_test.exs`, `breakdown_test.exs`, `timeseries_test.exs`, `auth_test.exs` and a dozen `query_*_test.exs` files, each `use PlausibleWeb.ConnCase` with `setup [:create_user, :create_site, :create_api_key, :use_api_key]` and assertions like `json_response(conn, 402)["error"]`. The agent writes its test there, with factories from `test/support/factory.ex`, and runs `mix deps.get`, `mix compile --warnings-as-errors` and `mix test` on the files it changed and then on the suite, as `.github/workflows/elixir.yml` does against `postgres:18` and `clickhouse-server:25.11`, and opens the pull request.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Implement the Linear issue given as a resource in this Phoenix codebase. Read the issue and its comments first. Make the change in the stats API controller, the query layer under it or the authorization plug the issue concerns, and keep the diff small. Add ExUnit tests under test/plausible_web/controllers/api using PlausibleWeb.ConnCase and the existing setup helpers. Start PostgreSQL and ClickHouse in the sandbox as CONTRIBUTING.md describes, run `mix deps.get` and `mix compile --warnings-as-errors`, then `mix test` on the changed files and on the suite, and fix what fails. Open a pull request whose description links the issue, explains the change and quotes the test result. Do not merge.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    },
    {
      "url": "https://linear.app/your-org/issue/ENG-42"
    }
  ]
}
```

1. **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.
2. **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.
3. **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

A pull request on its own branch comes back. Its description links the issue, names the controller function, query module or plug it changed, and quotes the ExUnit summary. The diff holds the change and its test in the matching `external_stats_controller/` file.

The pull request URL is on the session and in the `run.completed` webhook, and your handler posts it as a comment on the issue, since the sandbox has no Linear tools of its own. The transcript keeps the database start-up, the compile and each `mix test` command, so a reviewer can see that a change to `aggregate` was checked against `aggregate_test.exs` and the feature-access cases in it.

Before merging, a reviewer reads the change once for the Community Edition and once for the Enterprise one: the router wraps parts of itself in `on_ee`, and `test_helper.exs` has a `ce_test` mode that excludes `:ee_only` tests, so a change to a public endpoint needs to hold in both.

## Run it on every Linear issue labeled agent

Create an automation with a `linear` trigger on `linear.issue.labeled`, the hand-off label in `config.labels` and the team keys in `config.teams`. Labeling an issue starts one run with the issue as its context and this prompt; a label added by an app integration never fires it.

Add a `linear.issue.state_changed` trigger with `config.states` mapping a review state to `plan`, a read-only run that posts its plan on the issue, and a later state to `exec`, which opens the pull request. For a public API, the plan step is where a maintainer says whether a change belongs in `/api/v1/stats` or in the `/api/v2` query endpoint.

## Why run it on Rush?

A Plausible change cannot be tested without PostgreSQL and ClickHouse running, migrated and cleaned between runs, which is why `mix test` is an alias that does all of that. On Rush each ticket gets a sandbox that starts both, runs the suite and is discarded, and the output stays with the run.

The public stats API is a contract with everyone who scripts against it, so a pull request that quotes `aggregate_test.exs` and `query_validations_test.exs` passing carries weight a description cannot. Tickets run in parallel in separate sandboxes, each with its own databases.

## Frequently asked questions

### How does the agent read the Linear issue?

From the `resources` entry with its URL, read through your Linear connection and placed in the prompt before the run starts. Connect Linear once with `POST /me/connections {"type": "app", "name": "linear"}`; an issue URL with no connection answers `422 RESOURCE_NOT_ACCESSIBLE`.

### Can it post the pull request link on the issue?

Not from the sandbox; a Linear sign-in cannot yet be named in `connections`. Read the URL from the session or the webhook and post it from your handler.

### Does it need ClickHouse?

Yes. Events live in ClickHouse and the `test` alias ends with `clean_clickhouse`. The agent starts it in the sandbox, as `make clickhouse` does; CI uses `clickhouse-server:25.11.5.8-alpine` and `postgres:18`.

### Does it run the slow tests?

Not by default: `test_helper.exs` excludes `:slow`, `:minio` and `:migrations`, and CI adds them with `--include`. Put `mix test --include slow` in the prompt when the ticket touches what they cover.
