# Turn Linear Issues into Pull Requests for a FastAPI App

Label the issue and an agent implements it in your FastAPI routes and SQLModel models, adds the Alembic migration and pytest tests, and opens a linked pull request.

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

## What the agent does

The run starts with the issue in the prompt, read through your Linear connection from the `https://linear.app/<workspace>/issue/<ID>` resource, and the repository cloned beside it. For an issue like "add a soft delete for items with a restore endpoint" in an app shaped like fastapi/full-stack-fastapi-template, the agent reads `backend/pyproject.toml` (Python 3.14, `fastapi[standard]`, `sqlmodel`, `alembic`, `psycopg`, `pydantic-settings`, and a dev group with `pytest`, `coverage`, `mypy` in strict mode, `ty` and `ruff`), `uv.lock`, and the scripts the project runs: `scripts/prestart.sh` (`alembic upgrade head` then `app/initial_data.py`), `scripts/test.sh` (`coverage run -m pytest tests/` with `FASTAPI_ENV=development`, then `coverage report`) and `scripts/lint.sh` (`mypy app`, `ty check app`, `ruff check app`, `ruff format app --check`). `.github/workflows/test-backend.yml` shows the order CI uses: `docker compose up -d --wait db mailpit`, prestart, tests, and `coverage report --fail-under=90`.

It then reads the code the issue touches. `app/models.py` defines `Item` with a UUID id, `title` (1 to 255 characters), `description`, `created_at` and an `owner_id` foreign key with `ondelete="CASCADE"`, plus `ItemCreate`, `ItemUpdate`, `ItemPublic` and `ItemsPublic`. `app/api/routes/items.py` lists items for the owner (or every item for a superuser) with `count` and `data`, and its `read_item`, `update_item` and `delete_item` share the same two guards: 404 "Item not found" and 403 "Not enough permissions". `delete_item` calls `session.delete` and returns `Message("Item deleted successfully")`. The tests in `tests/api/routes/test_items.py` use the `client`, `superuser_token_headers`, `normal_user_token_headers` and `db` fixtures from `tests/conftest.py` and `create_random_item` from `tests/utils/item.py`.

The change follows those shapes. `Item` and `ItemPublic` gain `deleted_at: datetime | None`; `alembic revision --autogenerate -m "add deleted_at to item"` writes a migration in `app/alembic/versions/` next to the five that exist, in the same `op.add_column` form as `fe56fa70289e_add_created_at_to_user_and_item.py`. `read_items` and `read_item` filter on `Item.deleted_at == None`, `delete_item` sets `deleted_at` to `get_datetime_utc()` instead of deleting the row, and a new `POST /items/{id}/restore` clears it with the same 404 and 403 guards, returning `ItemPublic`. A `include_deleted` query flag on the list is added only if the issue asks for it; otherwise the description names it as a follow-up.

Tests come with the change: a deleted item disappears from the list and answers 404 on `GET`, `restore` brings it back and returns the item, a normal user restoring someone else's item gets 403, and the existing delete test's assertion on the message still holds. The agent runs `uv run bash scripts/test.sh`, keeps coverage above the 90 percent CI requires, runs `scripts/lint.sh` so strict mypy and ruff pass, and, because `frontend/src/client` is generated from the OpenAPI schema by `scripts/generate-client.sh`, regenerates the client so the new route is typed on the frontend too. The pull request description links the issue and quotes the test and coverage lines.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Do the work the Linear issue given as a resource describes: add a soft delete for items with a restore endpoint. Read the issue and its comments first. Follow this project's patterns: a nullable deleted_at on the Item model and ItemPublic, an Alembic autogenerated migration in app/alembic/versions, the list and read endpoints hiding deleted items, DELETE setting deleted_at instead of removing the row, and a POST /items/{id}/restore route with the same 404 and 403 guards as the other item routes. Add pytest tests beside tests/api/routes/test_items.py for delete, hidden-after-delete, restore and the permission refusal. Start Postgres, run bash scripts/prestart.sh, bash scripts/test.sh and bash scripts/lint.sh, regenerate the frontend client with scripts/generate-client.sh, and open a pull request whose description links the issue and quotes the coverage line. 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

What comes back is a pull request on a new branch with changes to `backend/app/models.py`, `backend/app/api/routes/items.py`, one new file under `backend/app/alembic/versions/`, new tests in `backend/tests/api/routes/test_items.py`, and the regenerated `frontend/src/client` with the restore operation and the `deleted_at` field. No other route, model or setting moves.

The description links the issue, states the rule in one line (deleted items are hidden and restorable by their owner or a superuser), and quotes the `pytest` summary, the `coverage report` total and the clean `lint.sh` run. The trajectory keeps the Alembic output, so a reviewer can see the migration was generated from the model rather than typed by hand.

Before merging, a reviewer checks that the migration's `downgrade` drops the column, that `read_items` filters deleted rows in both the superuser and the owner branch and in both the `count` and the `data` query, that `restore` on an item that is not deleted is handled deliberately, and that the frontend client diff contains only the generated changes.

## Run it on every Linear issue labeled agent

Create an automation in the Project with this prompt and a `linear` trigger on `linear.issue.labeled`, `config.labels` set to your hand-off label. Labeling starts one run with the issue as its context; a bot or integration that labels issues never fires it. Cap `max_runs_per_hour` so a triage session that labels twenty issues does not start twenty runs at once.

For tickets that need a design check first, add a `linear.issue.state_changed` trigger whose `config.states` maps one state to `plan` and the next to `exec`: the first run reads the code and comments its plan on the issue, the second does the work. Give the API key a webhook URL and read `run.completed` to post each pull request link where the team sees it.

## Why run it on Rush?

A FastAPI service with Alembic, strict mypy and a Postgres test database needs the database up, the migrations applied and the first superuser created before one test runs, and the coverage gate means a change without tests fails CI anyway. On Rush the agent does the setup in its own sandbox the way `test-backend.yml` does, and the pull request arrives with the test, coverage and lint lines already in it.

Every labeled issue becomes its own run with its own record, so a week of small tickets becomes a week of small pull requests, each traceable from the Linear issue to the migration, the tests and the commands that proved them. Nothing waits for a laptop to be free.

## Frequently asked questions

### Does it write the Alembic migration by hand?

No. It changes the SQLModel model and runs `alembic revision --autogenerate` against a running Postgres, then reads the generated file to confirm it adds and drops exactly the one column. Hand-written migrations drift from the model; generated ones do not.

### Will it keep the 90 percent coverage gate?

Yes. `scripts/test.sh` runs under `coverage`, and CI fails below 90 percent, so the agent adds tests for every new branch, including the 403 refusal on restore. The coverage total is quoted in the description.

### Does it touch the React frontend?

Only the generated client. `scripts/generate-client.sh` exports the OpenAPI schema and runs `openapi-ts` so `frontend/src/client` knows the new route. A restore button in the UI is a separate issue unless this one asks for it.

### How does it get Postgres and the first superuser?

It starts Postgres in the sandbox, sets `POSTGRES_*`, `FIRST_SUPERUSER` and `FIRST_SUPERUSER_PASSWORD` as `app/core/config.py` expects, and runs `scripts/prestart.sh`, which applies migrations and creates the superuser the tests sign in as.

### What if the issue is underspecified?

The agent picks the reading that matches the existing routes and says so in the pull request description, with the alternative named. It does not post to the issue itself, since a run cannot yet name a Linear connection.
