# Add OpenAPI Docs to a Flask API from a Linear Issue

Yes. An agent picks up the Linear issue, writes an OpenAPI document for your Flask blueprint, serves Swagger UI, tests that every route is listed and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-openapi-docs/flask/), in a Project that has your repository.

## What the agent does

The run starts with the issue text and the repository in its sandbox, and the agent reads the API before describing it. In a Flask app like `miguelgrinberg/microblog`, `app/api/__init__.py` declares a blueprint that `app/__init__.py` registers at `/api`, and three modules fill it. `app/api/users.py` has `GET /users/<id>`, `GET /users` with `page` and `per_page` capped at 100, `GET /users/<id>/followers` and `/following`, `POST /users`, which answers 201 with a `Location` header, and `PUT /users/<id>`, which aborts with 403 unless the token's user matches the id. `app/api/tokens.py` has `POST /tokens` behind HTTP Basic auth and `DELETE /tokens` answering 204. `app/api/auth.py` builds both schemes with `Flask-HTTPAuth`, and `app/api/errors.py` shapes every error as `{"error": ..., "message": ...}`. `requirements.txt` pins `Flask==3.0.0`, `Flask-HTTPAuth==4.8.0` and `Flask-SQLAlchemy==3.1.1`, and nothing in it generates OpenAPI.

The response schemas come from the models. `User.to_dict` in `app/models.py` returns `id`, `username`, `last_seen`, `about_me`, `post_count`, `follower_count`, `following_count` and a `_links` object with `self`, `followers`, `following` and `avatar`, adding `email` only when asked. `PaginatedAPIMixin.to_collection_dict` wraps a list as `items` plus `_meta` with `page`, `per_page`, `total_pages` and `total_items`, and `_links` with `self`, `next` and `prev`. The agent writes these as named schemas once and references them from every route, so the collection routes share one shape.

Rather than restructure the blueprint around a framework, it writes `app/api/openapi.yaml` by hand as an OpenAPI 3.1 document: the eight operations, their parameters, the request body for creating and updating a user, the 201 with `Location`, the 400 from `bad_request`, the 401 the auth error handlers return, the 403 and 404, and two security schemes, `basicAuth` on `POST /tokens` and `bearerAuth` everywhere else. Two new routes in `app/api/docs.py` serve the document at `GET /api/openapi.json` and a Swagger UI page at `GET /api/docs`, loading the UI assets from the pinned CDN version named in the file, so `requirements.txt` gains only `PyYAML` for parsing and `openapi-spec-validator` for the test.

The test goes in `tests.py`, which already builds the app with an in-memory SQLite `TestConfig`. One case validates the document with `openapi-spec-validator`; a second walks `app.url_map`, keeps every rule under `/api` except the two docs routes, converts Flask's `<int:id>` to `{id}`, and asserts each method and path appears in `paths`, so a route added later without documentation fails the suite. The agent runs `python -m unittest`, then starts the app and opens `/api/docs` in the sandbox's browser to confirm the UI renders and can call `POST /tokens`, before opening the pull request.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add OpenAPI docs to the Flask API. Serve an OpenAPI 3.1 document for every route in the api blueprint and a Swagger UI page, and add a test that fails when a route is missing from the document.\" Do that work in this repository. Read app/__init__.py, app/api/__init__.py, app/api/users.py, app/api/tokens.py, app/api/auth.py, app/api/errors.py, the to_dict and to_collection_dict methods in app/models.py, requirements.txt and tests.py first. Write app/api/openapi.yaml by hand with named schemas for the user and the paginated collection, the error shape, the 201 Location header, and basicAuth on POST /tokens with bearerAuth elsewhere. Serve it at GET /api/openapi.json and Swagger UI at GET /api/docs from a pinned CDN version. Add tests to tests.py that validate the document and assert every api blueprint rule appears in it. Pin any new package in requirements.txt. Run the test suite, open /api/docs in the browser, and open a pull request that links back to the issue.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

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 its own branch that links the Linear issue, with `app/api/openapi.yaml`, `app/api/docs.py`, the import that registers it in `app/api/__init__.py`, the two new cases in `tests.py`, and `requirements.txt` with the new pins. The description lists the eight operations the document covers, the two security schemes, and the CDN version the UI loads.

The transcript holds the unittest run with its output, the app start, and a screenshot of `/api/docs` rendering the document in the sandbox's browser. A reviewer compares the schema for a user against `to_dict` in `app/models.py`, checks that `PUT /users/{id}` documents the 403, and reads the test to confirm it walks `app.url_map` rather than a hard-coded list.

Merging the pull request closes the issue through the link in its description. A follow-up message to the same session can ask for the document to be generated from docstrings instead, or for the docs route to be hidden outside development, and the agent continues in the same sandbox with the app already installed.

## Run it on every Linear issue labelled agent

In the console, add an automation to the Project with a Linear trigger on `linear.issue.labeled`, filtered to the `agent` label, and this prompt. When someone labels an issue, the run starts with the issue's text after the prompt and opens its own pull request on its own branch. Changes made by an app integration never fire the trigger.

From your own server, the same request names the issue in `resources` as `https://linear.app/<workspace>/issue/<ID>`, read through your Linear connection, with the repository beside it. Set a `webhook_url` on the API key to receive `run.completed` with the `session_url` for the pull request.

## Why run it on Rush?

Writing an OpenAPI document by hand means reading every route and every serializer and keeping the two in sync, then checking the result in a browser. The agent does that reading in its own sandbox, runs the app there to look at the rendered UI, and keeps every command and the screenshot on the session, so nothing runs on your laptop.

The pull request is checkable without trust. The test that walks `app.url_map` is in the diff, the unittest output is in the transcript, and the screenshot shows the UI. Label five documentation issues across five services and five runs start, each in its own sandbox and branch, each ending in a pull request that links its issue.

## Frequently asked questions

### Why write the YAML by hand instead of using flask-smorest or APIFlask?

Because those frameworks replace the blueprint's view functions and error handling with their own decorators and schemas, which is a rewrite of the API rather than documentation of it. A hand-written document plus a test that compares it with `app.url_map` documents the routes as they are. If you want generated docs, say so in the input and the agent plans that migration instead.

### Does the document need authentication to read?

No. `/api/openapi.json` and `/api/docs` are public in the pull request, since the document describes only the API's shape. If the API should not advertise itself, a follow-up can put the two routes behind `token_auth.login_required` or register them only when a config flag is set.

### How does the test know every route?

It reads Flask's `app.url_map` at test time, keeps the rules whose endpoint starts with `api.` and skips the docs routes, then checks each method and path against the document. A route added to the blueprint without a matching entry fails the suite.

### Will it change my route handlers or the JSON they return?

No. The handlers in `app/api/users.py` and `app/api/tokens.py` are untouched; the document is written to match them. If the agent finds a response that does not match what the code returns, it says so in the pull request rather than changing the code.
