Skip to main content

Cursor pagination and sorting

Operations using cursor pagination

listArrayItems and lookupArrayItems use forward-only cursor pagination. There are no backward-pagination arguments.

Requesting the first page

Defaults:

InputDefault
first20
afterBeginning of results
sortid
sortDirectionasc

first must be between 1 and 500 inclusive.

Example: First page sorted by date

query ListRecentFavorites($input: ListArrayItemsInput!) {
listArrayItems(input: $input) {
totalCount
data { id itemId value sortDate }
pageInfo { hasNextPage endCursor }
}
}
{
"input": {
"scope": "USER",
"key": "content:favorites",
"first": 2,
"sort": "sortDate",
"sortDirection": "desc"
}
}
{
"data": {
"listArrayItems": {
"totalCount": 2,
"data": [
{
"id": "66b100000000000000000001",
"itemId": "movie:100",
"value": {
"title": "Example Movie",
"mediaType": "MOVIE"
},
"sortDate": "2026-09-18T10:30:00.000Z"
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "<opaque cursor returned by the service>"
}
}
}
}

Requesting the next page

Pass the previous page's endCursor as after and keep the normalized query unchanged:

{
"input": {
"scope": "USER",
"key": "content:favorites",
"first": 2,
"after": "<opaque cursor returned by the service>",
"sort": "sortDate",
"sortDirection": "desc"
}
}

The page size may change between requests. The cursor remains bound to the operation, query, scope, key, sort, and direction.

Page information

  • hasNextPage indicates whether another matching item exists after the returned page.
  • endCursor identifies the position of the final returned item for the next request.
  • An empty page has data: [], hasNextPage: false, and endCursor: null.

Cursor rules

Cursors are opaque

Cursors are service-generated positions. Do not decode, construct, modify, or treat them as permanent item identities.

Query binding

A listArrayItems cursor is bound to the operation, authenticated scope, key, sort property, and sort direction. A lookupArrayItems cursor is additionally bound to the canonical selector sets.

Using a cursor with a different operation, scope, key, sort, direction, or lookup selector set fails with CURSOR_CONSTRAINT. Selector order does not matter after normalization.

Changing page size

first is not cursor-bound and may change between requests, subject to the 1–500 limit.

Sorting

Supported sort properties

ValueMeaning
idService-generated ID
dateModifiedService-owned last-modified time
sortStringCustomer-provided string
sortNumberCustomer-provided integer
sortDateCustomer-provided date

Sort direction and ties

sortDirection is asc or desc. Equal primary values use the service-generated ID as the deterministic secondary key, in the same direction.

Items without the selected sort field remain traversable. Do not assume selector order controls result order.

Understanding totalCount

listArrayItems consistency

listArrayItems.totalCount describes the complete array, but the count and returned page are not snapshot-consistent. Concurrent writes can make them describe different moments.

lookupArrayItems consistency

lookupArrayItems.totalCount and its page share one request snapshot. Separate cursor requests use separate snapshots.

Concurrent changes during traversal

Cursor pagination avoids offset shifting, but it is not a multi-request snapshot. Inserts, deletes, and changes to an item's active sort value can affect later pages. A deleted cursor anchor does not prevent traversal from continuing.

Pagination errors

Invalid first returns PAGINATION_CONSTRAINT with field: first and constraint: range.

Malformed, unsupported-version, or query-mismatched cursors return CURSOR_CONSTRAINT with field: after and a corresponding cursor constraint. Inspect errors[].extensions; do not parse message text.

See Errors and limits.

Was this page helpful?