All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.

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.