Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when adding pagination to a GraphQL schema: model the pageable field with the Cursor Connections structure (Connection/Edge/node/cursor/pageInfo, first/after), not the REST-style limit/offset list a cheaper model reaches for by default.
.claude/skills/graphql-cursor-connections/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flashlowest | 89% | 10 |
| gemini-3.1-pro-preview | 100% | 1 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +14% | +71% | 0% | 22 | 54d ago |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✗→✓ | ▲ Improved | — | — |
| case-19 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✓→✓ | = Same ✓ | — | — |
| case-16 | ✗→✗ | = Same ✗ | — | — |
When a GraphQL field exposes a list that clients page through, model it with the Cursor Connections structure: a {Type}Connection return type wrapping an edges list (each edge carrying node + cursor) and a pageInfo object, paged by first/after arguments with opaque string cursors. Apply this to any field that returns a browsable, scrollable, or "load-more" collection — never a bare list with offset arguments.
name is the entity plus the Connection suffix (Article -> ArticleConnection). Never return [Article!]! directly for a field the caller pages.
edges: [{Type}Edge!]! and pageInfo: PageInfo!.A totalCount: Int may be added alongside them, but it never replaces either one.
{Type}Edge and holds two fields: node: {Type}!(the record itself) and cursor: String! (the position marker). The cursor lives on the EDGE, never on the record/node type.
PageInfo carries the paging flags. At minimum hasNextPage: Boolean! andendCursor: String for forward paging. Add hasPreviousPage: Boolean! and startCursor: String when backward paging is offered. These stay inside the PageInfo object; they are never flattened onto the connection root.
first/after. Forward paging takes first: Int (how many) andafter: String (the cursor to resume past). Backward paging takes last: Int and before: String. Never limit/offset, page/pageSize, skip/take, or perPage.
String — an opaque token (commonly base64),not a raw integer offset, page number, or exposed database id. Callers treat it as a black box.
BEFORE — the base's reflexive REST-style default (offset list):
graphqltype Query { articles(limit: Int, offset: Int): [Article!]! }
AFTER — the Cursor Connections form:
graphqltype Query { articles(first: Int, after: String): ArticleConnection! } type ArticleConnection { edges: [ArticleEdge!]! pageInfo: PageInfo! } type ArticleEdge { node: Article! cursor: String! } type PageInfo { hasNextPage: Boolean! endCursor: String }
BEFORE — an ad-hoc wrapper with the wrong field names:
graphqltype EmployeePage { items: [Employee!]! nextCursor: String hasMore: Boolean! total: Int }
AFTER — rename to the spec fields; items becomes edges, and the flat flags move into pageInfo:
graphqltype EmployeeConnection { edges: [EmployeeEdge!]! pageInfo: PageInfo! totalCount: Int } type EmployeeEdge { node: Employee! cursor: String! }
BEFORE — cursor placed on the record type:
graphqltype Book { id: ID! title: String! cursor: String! }
AFTER — the position marker belongs to the edge, not the record:
graphqltype BookEdge { node: Book! cursor: String! }
hasPreviousPage andstartCursor, but keep hasNextPage and endCursor.
edges: [] and pageInfo.hasNextPage: false — notnull and not an empty bare list.
totalCount: Int to the connection ALONGSIDE edges/pageInfo;do not swap the connection for { items, total }.
orderBy argument sit next to first/afteron the same field and do not change the connection shape.
nodes: [{Type}!]! helper is acceptable only IN ADDITION toedges, never as a replacement — clients still need edge cursors to page.
{Type}Connection and the edge {Type}Edge.[{Type}!]! directly for a field the caller pages.cursor on the edge.cursor on the record/node type.first/after (and last/before for backward paging).limit/offset, page/pageSize, skip/take, or perPage.hasNextPage and endCursor inside a pageInfo object.hasMore/nextCursor.String.[{Type}!]! with limit/offset arguments — the reflexive HTTP/REST carry-over.items, results, hasMore, nextCursor, or totalPages instead of the specfield names.
hasNextPage/endCursor to the connection root instead of nesting them in PageInfo.cursor to the record type rather than to the edge.Connection/Edge type-name suffixes.{Type}Connection!, not a bare list.edges: [{Type}Edge!]! and pageInfo: PageInfo!.node plus cursor: String! (cursor on the edge).PageInfo has hasNextPage and endCursor.first/after (and last/before).| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/2/2026 | +54% |
Other measured skills in the registry, with their headline benchmark lift.