# Add Cursor Pagination to a Gin API from a Linear Issue

Yes. An agent reads the labelled Linear issue, adds an opaque cursor to a Gin API's list endpoints beside offset paging, tests it, and opens a pull request.

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

## What the agent does

The agent reads the issue, then the code the issue is about. In an API shaped like gothinkster/golang-gin-realworld-example-app, `go.mod` declares Go 1.21 with Gin 1.10.0, GORM 1.25.12 on the SQLite driver, and testify, and `AGENTS.md` lays out one package per module: `articles/`, `users/` and `common/`, each with `models.go`, `routers.go`, `serializers.go` and a `unit_test.go`. The list endpoints are `ArticleList` and `ArticleFeed` in `articles/routers.go`. Both read `limit` and `offset` as query strings and pass them to `FindManyArticle(tag, author, limit, offset, favorited)` and `GetArticleFeed(limit, offset)` in `articles/models.go`, where `strconv.Atoi` turns a missing or bad value into offset 0 and limit 20, and four branches apply `Offset().Limit()` to the tag, author, favorited and unfiltered queries.

Offset stays, because the RealWorld API contract and the existing tests use it; the cursor is added beside it. `ArticleModel` embeds `gorm.Model`, so every row has an `ID` and an `UpdatedAt`, and `ArticleResponse` in `serializers.go` already formats `updatedAt` as `2006-01-02T15:04:05.999Z`. The agent sorts on `updated_at DESC, id DESC`, encodes the last row's pair as a base64 string, and reads a `cursor` query parameter in both handlers: when present it adds a `WHERE (updated_at, id) < (?, ?)` clause through the same GORM query the branch already builds, fetches `limit + 1` rows, and returns the extra row's cursor as `nextCursor` beside `articlesCount` in the JSON body. A cursor that fails to decode answers with the same `common.NewError` shape the handlers already return for a bad parameter.

The feed query in `GetArticleFeed` already orders by `updated_at desc` over the followed authors' ids, so the cursor clause slots into that `Where`. The tag and author branches reach articles through GORM associations, where `Offset` and `Limit` are applied to the association lookup; the agent moves the cursor condition onto the final `Find` that preloads `Author.UserModel` and `Tags`, and says in the pull request where the ordering changed so a reviewer can compare it to the old branch.

Tests go into `articles/unit_test.go`, next to the existing `FindManyArticle` cases that call it with `"10", "0"` and with invalid strings, and the feed test that requests `/api/articles/feed?limit=5&offset=0` with a mocked token from `common.HeaderTokenMock`. New cases walk a seeded list page by page with the cursor and assert that no slug repeats and none is skipped, that a page shorter than the limit carries no `nextCursor`, and that a malformed cursor answers an error instead of the first page. The agent runs `go test ./...`, then `bash scripts/gofmt.sh` and `bash scripts/coverage.sh`, the two scripts `.github/workflows/ci.yml` runs on Go 1.21, 1.22 and 1.23, against the SQLite file `common.GetTestDBPath` points at.

## 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 cursor-based pagination to the article list and feed endpoints. Keep limit and offset working for existing clients. Add an opaque cursor parameter and return nextCursor in the response so clients can page without skipping or repeating articles when new ones are published.\" Read go.mod, AGENTS.md, articles/routers.go, articles/models.go, articles/serializers.go and articles/unit_test.go first. Sort on updated_at and id, encode the cursor as base64, apply it in every branch of FindManyArticle and in GetArticleFeed, answer a malformed cursor with the existing error shape, add tests that walk pages with the cursor, run go test ./..., scripts/gofmt.sh and scripts/coverage.sh, and open a pull request that links 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. The diff touches `articles/models.go` for the cursor type, its encoding and the extra `Where` and ordering in `FindManyArticle` and `GetArticleFeed`, `articles/routers.go` for reading `cursor` and returning `nextCursor`, and `articles/unit_test.go` for the paging tests. `go.mod` and `go.sum` are unchanged, because base64 and the comparison come from the standard library and GORM. The description quotes the issue, shows one request and response pair with a cursor, and states that `limit` and `offset` behave as before.

The checks are the proof. The `CI` workflow runs on the pull request with a matrix of three Go versions and executes the gofmt check and the coverage script, and the session's trajectory at `GET /o/{org}/sessions/{id}/messages` keeps every `go test` invocation with its output. A reviewer reads the paging test passing rather than trusting the description.

Before merging, a reviewer checks that the ordering is stable on ties, which is why the cursor carries the id as well as the timestamp; that the tag and author branches, which reach articles through associations, return the same rows with the cursor as without it; and that `articlesCount` still reports the total rather than the page size. Nothing reaches main until the pull request merges.

## Run it on every Linear issue labelled agent

Sign in to Linear once with `POST /me/connections {"type": "app", "name": "linear"}` and finish the authorize URL in the console. Then add an automation to the Project with this prompt and a `linear` trigger on `linear.issue.labeled`, naming the label in `config.labels`. The trigger adds the issue after the prompt and fills in no placeholder, so the agent reads the endpoints and the parameter names from the issue itself. The automation's `max_runs_per_hour`, between 1 and 30, caps how many labelled issues it takes on, each in its own sandbox with its own pull request.

From your own server instead, a handler that receives Linear's webhook for the label sends `POST /o/{org}/sessions` with the repository and `https://linear.app/<workspace>/issue/ENG-42` in `resources`, which puts the issue text in the prompt through your Linear connection, and a `request_id` derived from the issue id so a redelivered webhook returns the run it already started. Give that key a `webhook_url` and Rush POSTs `run.completed` or `run.failed` when the turn ends, with `data.session_url` to read for the pull request link.

## Why run it on Rush?

Cursor pagination is easy to describe and easy to get subtly wrong: a tie on the sort column, a filter branch that applies the limit before the ordering, a cursor that decodes to the wrong field. The way to know it is right is a test that walks every page of a seeded database and checks for gaps and repeats, and that is tedious to do by hand on a laptop beside other work. On Rush the agent writes that test in its own sandbox, runs `go test ./...` against the SQLite file the repository's tests already use, and pushes only when the walk is clean.

The pull request is the deliverable and the evidence. The diff shows exactly which branches of `FindManyArticle` changed, the test shows the walk, the repository's `CI` workflow runs it on three Go versions, and the trajectory keeps every command with its output. Several labelled issues become several sandboxes and several independent pull requests, not a queue on one machine.

## Frequently asked questions

### Will existing clients that send limit and offset still work?

Yes. The offset path is untouched: a request without `cursor` goes through the same `strconv.Atoi` defaults of offset 0 and limit 20 as before, and the existing tests for those cases still pass. The cursor is an additional parameter, and `nextCursor` is an additional field in the response, so a client that ignores it sees the same body shape as today.

### What is inside the cursor?

The `updated_at` timestamp and the `id` of the last row on the page, base64-encoded so clients treat it as opaque. Both come from the `gorm.Model` every `ArticleModel` embeds. A cursor that does not decode to that pair answers with the `common.NewError` shape the handlers already use for a bad parameter, rather than silently returning the first page.

### Why sort on updated_at rather than created_at?

Because the feed in `GetArticleFeed` already orders by `updated_at desc`, and the issue asks for stable paging while articles change. If you would rather page on creation time, say so in the issue; the change is the column name in the ordering and the cursor, and the tests walk the pages the same way.

### Does this change the database schema?

No migration is needed. `updated_at` and `id` exist on every table that embeds `gorm.Model`, which `hello.go` migrates with `AutoMigrate`. The pull request may add an index on `(updated_at, id)` through a GORM tag if the agent finds the query plan needs it, and the description says so if it does.
