Skip to content

Pagination

Standard patterns for paginated list operations


Functions returning lists SHOULD support pagination to handle large result sets efficiently. Mesh defines three pagination styles that cover common use cases.


Pagination parameters MUST be nested in a pagination object within arguments:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_123",
"call": {
"function": "orders.list",
"version": "1",
"arguments": {
"customer_id": 42,
"status": "active",
"pagination": {
"limit": 25,
"cursor": "eyJpZCI6MTAwfQ"
}
}
}
}
  • Avoids collision with function arguments (e.g., a function might have its own limit parameter)
  • Consistent access pattern across all paginated functions
  • Clear separation of concerns

Simple pagination using limit and offset. Familiar from SQL.

Request:

{
"pagination": {
"limit": 25,
"offset": 0
}
}

Response:

{
"result": {
"items": [...],
"pagination": {
"limit": 25,
"offset": 0,
"total": 150,
"has_more": true
}
}
}
FieldTypeDescription
limitintegerMaximum items to return
offsetintegerNumber of items to skip
totalintegerTotal items available (optional)
has_morebooleanWhether more items exist
  • Simple to implement and understand
  • Allows “jump to page N”
  • Performance degrades with large offsets
  • Results can shift if data changes between requests

Use when: Small datasets, admin interfaces, or when random page access is needed.


Opaque cursor token that encodes position. Server generates cursor, client passes it back.

First Request:

{
"pagination": {
"limit": 25
}
}

First Response:

{
"result": {
"items": [...],
"pagination": {
"limit": 25,
"next_cursor": "eyJpZCI6MTAwLCJkaXIiOiJuZXh0In0",
"prev_cursor": null,
"has_more": true
}
}
}

Subsequent Request:

{
"pagination": {
"limit": 25,
"cursor": "eyJpZCI6MTAwLCJkaXIiOiJuZXh0In0"
}
}
FieldTypeDescription
limitintegerMaximum items to return
cursorstringOpaque cursor from previous response
next_cursorstringCursor for next page (null if none)
prev_cursorstringCursor for previous page (null if none)
has_morebooleanWhether more items exist in this direction

Cursors SHOULD be:

  • Opaque to clients (implementation detail)
  • URL-safe (base64url encoded)
  • Tamper-resistant (optionally signed)

Example cursor payload (before encoding):

{
"id": 100,
"created_at": "2024-01-15T10:30:00Z",
"dir": "next"
}
  • Efficient for any position (no offset scanning)
  • Stable during concurrent modifications
  • Cannot jump to arbitrary page
  • Cursor may expire or become invalid

Use when: Large datasets, infinite scroll, or when consistency matters.


Pagination using explicit field values. Ideal for feeds and timelines.

Request (ID-based):

{
"pagination": {
"limit": 25,
"after_id": "msg_abc123",
"before_id": null
}
}

Request (Time-based):

{
"pagination": {
"limit": 25,
"since": "2024-01-15T10:00:00Z",
"until": "2024-01-15T12:00:00Z"
}
}

Response:

{
"result": {
"items": [...],
"pagination": {
"limit": 25,
"newest_id": "msg_xyz789",
"oldest_id": "msg_abc124",
"has_newer": true,
"has_older": true
}
}
}
FieldTypeDescription
limitintegerMaximum items to return
after_idstringReturn items after this ID (exclusive)
before_idstringReturn items before this ID (exclusive)
sincestringReturn items after this timestamp (ISO 8601)
untilstringReturn items before this timestamp (ISO 8601)
FieldTypeDescription
newest_idstringID of newest item in response
oldest_idstringID of oldest item in response
has_newerbooleanWhether newer items exist
has_olderbooleanWhether older items exist
  • after_id + limit — Get next N items after ID
  • before_id + limit — Get previous N items before ID
  • since + until — Get items in time range
  • since + limit — Get N items since timestamp
  • Perfect for polling (“give me everything since last fetch”)
  • Efficient keyset queries
  • Requires monotonic IDs or indexed timestamps
  • More complex query logic

Use when: Activity feeds, timelines, event logs, or polling scenarios.


Functions MAY support one or more pagination styles. Servers SHOULD:

  1. Document which style(s) each function supports
  2. Return an error if unsupported pagination parameters are provided
  3. Apply sensible defaults when pagination is omitted

Servers SHOULD:

  • Define a default limit (e.g., 25)
  • Define a maximum limit (e.g., 100)
  • Return an error if requested limit exceeds maximum
{
"errors": [{
"code": "INVALID_ARGUMENTS",
"message": "Limit exceeds maximum of 100",
"retryable": false,
"source": { "pointer": "/call/arguments/pagination/limit" },
"details": {
"max_limit": 100,
"requested": 500
}
}]
}

When no items match:

{
"result": {
"items": [],
"pagination": {
"limit": 25,
"has_more": false
}
}
}

Cursor-based example:

cursor = null
do {
response = call("orders.list", {
pagination: { limit: 25, cursor: cursor }
})
process(response.result.items)
cursor = response.result.pagination.next_cursor
} while (response.result.pagination.has_more)

Keyset-based example:

last_id = null
loop {
response = call("events.list", {
pagination: { limit: 100, after_id: last_id }
})
if (response.result.items.length > 0) {
process(response.result.items)
last_id = response.result.pagination.newest_id
}
sleep(interval)
}

Functions SHOULD advertise pagination support via mesh.describe:

{
"result": {
"function": "orders.list",
"pagination": {
"styles": ["cursor", "offset"],
"default_limit": 25,
"max_limit": 100
}
}
}

// Request
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_page",
"call": {
"function": "users.list",
"version": "1",
"arguments": {
"role": "admin",
"pagination": {
"limit": 10,
"offset": 20
}
}
}
}
// Response
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_page",
"result": {
"items": [
{ "id": 21, "name": "Alice" },
{ "id": 22, "name": "Bob" }
],
"pagination": {
"limit": 10,
"offset": 20,
"total": 52,
"has_more": true
}
}
}
// Request
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_cursor",
"call": {
"function": "orders.list",
"version": "1",
"arguments": {
"customer_id": 42,
"pagination": {
"limit": 25,
"cursor": "eyJpZCI6MTAwfQ"
}
}
}
}
// Response
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_cursor",
"result": {
"items": [
{ "order_id": 101, "status": "shipped" },
{ "order_id": 102, "status": "pending" }
],
"pagination": {
"limit": 25,
"next_cursor": "eyJpZCI6MTI1fQ",
"prev_cursor": "eyJpZCI6MTAwLCJkaXIiOiJwcmV2In0",
"has_more": true
}
}
}
// Request - Get recent messages
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_timeline",
"call": {
"function": "messages.list",
"version": "1",
"arguments": {
"channel_id": "general",
"pagination": {
"limit": 50,
"before_id": "msg_xyz789"
}
}
}
}
// Response
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_timeline",
"result": {
"items": [
{ "id": "msg_xyz788", "text": "Hello", "created_at": "2024-01-15T10:29:00Z" },
{ "id": "msg_xyz787", "text": "Hi", "created_at": "2024-01-15T10:28:00Z" }
],
"pagination": {
"limit": 50,
"newest_id": "msg_xyz788",
"oldest_id": "msg_xyz740",
"has_newer": true,
"has_older": true
}
}
}
// Request - Poll for new events
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_poll",
"call": {
"function": "events.list",
"version": "1",
"arguments": {
"stream": "orders",
"pagination": {
"limit": 100,
"since": "2024-01-15T10:00:00Z"
}
}
}
}
// Response
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_poll",
"result": {
"items": [
{ "id": "evt_001", "type": "order.created", "timestamp": "2024-01-15T10:05:00Z" },
{ "id": "evt_002", "type": "order.paid", "timestamp": "2024-01-15T10:07:00Z" }
],
"pagination": {
"limit": 100,
"newest_id": "evt_002",
"oldest_id": "evt_001",
"has_newer": false,
"has_older": true
}
}
}