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 with this body:
{
"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"
}
]
}- 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 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.
The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.
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.