Pagination
Pagination
Section titled “Pagination”Standard patterns for paginated list operations
Overview
Section titled “Overview”Functions returning lists SHOULD support pagination to handle large result sets efficiently. Mesh defines three pagination styles that cover common use cases.
Pagination Object
Section titled “Pagination Object”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" } } }}Why Nested?
Section titled “Why Nested?”- Avoids collision with function arguments (e.g., a function might have its own
limitparameter) - Consistent access pattern across all paginated functions
- Clear separation of concerns
Pagination Styles
Section titled “Pagination Styles”1. Offset-Based
Section titled “1. Offset-Based”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 } }}Fields
Section titled “Fields”| Field | Type | Description |
|---|---|---|
limit | integer | Maximum items to return |
offset | integer | Number of items to skip |
total | integer | Total items available (optional) |
has_more | boolean | Whether more items exist |
Trade-offs
Section titled “Trade-offs”- 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.
2. Cursor-Based
Section titled “2. Cursor-Based”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" }}Fields
Section titled “Fields”| Field | Type | Description |
|---|---|---|
limit | integer | Maximum items to return |
cursor | string | Opaque cursor from previous response |
next_cursor | string | Cursor for next page (null if none) |
prev_cursor | string | Cursor for previous page (null if none) |
has_more | boolean | Whether more items exist in this direction |
Cursor Encoding
Section titled “Cursor Encoding”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"}Trade-offs
Section titled “Trade-offs”- 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.
3. Keyset-Based (Time/ID)
Section titled “3. Keyset-Based (Time/ID)”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 } }}Fields (Request)
Section titled “Fields (Request)”| Field | Type | Description |
|---|---|---|
limit | integer | Maximum items to return |
after_id | string | Return items after this ID (exclusive) |
before_id | string | Return items before this ID (exclusive) |
since | string | Return items after this timestamp (ISO 8601) |
until | string | Return items before this timestamp (ISO 8601) |
Fields (Response)
Section titled “Fields (Response)”| Field | Type | Description |
|---|---|---|
newest_id | string | ID of newest item in response |
oldest_id | string | ID of oldest item in response |
has_newer | boolean | Whether newer items exist |
has_older | boolean | Whether older items exist |
Combining Parameters
Section titled “Combining Parameters”after_id+limit— Get next N items after IDbefore_id+limit— Get previous N items before IDsince+until— Get items in time rangesince+limit— Get N items since timestamp
Trade-offs
Section titled “Trade-offs”- 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.
Server Implementation
Section titled “Server Implementation”Choosing a Style
Section titled “Choosing a Style”Functions MAY support one or more pagination styles. Servers SHOULD:
- Document which style(s) each function supports
- Return an error if unsupported pagination parameters are provided
- Apply sensible defaults when pagination is omitted
Default Limits
Section titled “Default Limits”Servers SHOULD:
- Define a default
limit(e.g., 25) - Define a maximum
limit(e.g., 100) - Return an error if requested
limitexceeds 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 } }]}Empty Results
Section titled “Empty Results”When no items match:
{ "result": { "items": [], "pagination": { "limit": 25, "has_more": false } }}Client Behavior
Section titled “Client Behavior”Iterating Pages
Section titled “Iterating Pages”Cursor-based example:
cursor = nulldo { 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)Polling for Updates
Section titled “Polling for Updates”Keyset-based example:
last_id = nullloop { 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)}Discovery
Section titled “Discovery”Functions SHOULD advertise pagination support via mesh.describe:
{ "result": { "function": "orders.list", "pagination": { "styles": ["cursor", "offset"], "default_limit": 25, "max_limit": 100 } }}Examples
Section titled “Examples”Offset Pagination
Section titled “Offset Pagination”// 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 } }}Cursor Pagination
Section titled “Cursor Pagination”// 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 } }}Keyset Pagination (Timeline)
Section titled “Keyset Pagination (Timeline)”// 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 } }}Keyset Pagination (Polling)
Section titled “Keyset Pagination (Polling)”// 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 } }}