Pagination
Memory Service uses cursor-based pagination for all list endpoints. Instead of page numbers or offsets, you pass the cursor from the previous response to fetch the next batch. This approach is efficient, consistent under concurrent writes, and scales well for large datasets.
How It Works
Forward-only paginated endpoints return a response with two fields:
{
"data": [ ... ],
"afterCursor": "d8c78c2b-b24a-46f4-86fd-22f066f74526"
}
| Field | Description |
|---|---|
data | Array of results for the current page |
afterCursor | Cursor to pass in the next request for more results. null when there are no more pages. |
To paginate through results:
- Make the initial request (optionally with a
limit) - Check
afterCursorin the response - If
afterCursoris notnull, pass it as theafterCursorparameter in the next request - Repeat until
afterCursorisnull
Conversation entry endpoints also return beforeCursor and support opening at
the newest page. Those entry-specific controls are described below.
Paginated Endpoints
All paginated endpoints share the same pattern but differ in defaults and parameter placement.
Agent API
| Endpoint | Cursor Param | Limit Param | Default Limit | Max Limit |
|---|---|---|---|---|
GET /v1/conversations | afterCursor (query) | limit (query) | 20 | 1000 |
GET /v1/conversations/{id}/entries | afterCursor / beforeCursor / tail (query) | limit (query) | 50 | 1000 |
GET /v1/conversations/{id}/memberships | afterCursor (query) | limit (query) | 50 | 1000 |
POST /v1/conversations/search | afterCursor (body) | limit (body) | 20 | 1000 |
GET /v1/conversations/unindexed | afterCursor (query) | limit (query) | 100 | 1000 |
GET /v1/ownership-transfers | afterCursor (query) | limit (query) | 50 | 1000 |
GET /v1/conversations/{id}/forks is intentionally absent: it returns one complete navigation snapshot containing conversationIds and the fork points visible in the requested conversation.
Admin API
| Endpoint | Cursor Param | Limit Param | Default Limit | Max Limit |
|---|---|---|---|---|
GET /v1/admin/conversations | afterCursor (query) | limit (query) | 100 | 1000 |
GET /v1/admin/conversations/{id}/entries | afterCursor / beforeCursor / tail (query) | limit (query) | 50 | 1000 |
GET /v1/admin/conversations/{id}/memberships | afterCursor (query) | limit (query) | 50 | 1000 |
POST /v1/admin/conversations/search | afterCursor (body) | limit (body) | 20 | 1000 |
GET /v1/admin/attachments | afterCursor (query) | limit (query) | 50 | 1000 |
Listing Conversations
Fetch conversations in pages of a given size using the limit and afterCursor query parameters.
First page
curl "http://localhost:8080/v1/conversations?limit=2" \
-H "Authorization: Bearer <token>"
Response:
{
"data": [
{
"id": "d8c78c2b-b24a-46f4-86fd-22f066f74526",
"title": "Support chat",
"ownerUserId": "user_1234",
"createdAt": "2025-01-10T14:32:05Z",
"updatedAt": "2025-01-10T14:45:12Z",
"accessLevel": "owner"
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"title": "Design discussion",
"ownerUserId": "user_1234",
"createdAt": "2025-01-11T09:00:00Z",
"updatedAt": "2025-01-11T09:15:00Z",
"accessLevel": "owner"
}
],
"afterCursor": "660e8400-e29b-41d4-a716-446655440001"
}
Next page
Pass the afterCursor value as the afterCursor parameter:
curl "http://localhost:8080/v1/conversations?limit=2&afterCursor=660e8400-e29b-41d4-a716-446655440001" \
-H "Authorization: Bearer <token>"
Response (last page):
{
"data": [
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"title": "Bug triage",
"ownerUserId": "user_1234",
"createdAt": "2025-01-12T10:00:00Z",
"updatedAt": "2025-01-12T10:30:00Z",
"accessLevel": "owner"
}
],
"afterCursor": null
}
An afterCursor of null means there are no more results.
Listing Entries
Entry responses remain chronological, but they support navigation in both directions:
| Control | Result |
|---|---|
| no cursor | The oldest page. |
afterCursor={id} | Entries strictly after the cursor. |
beforeCursor={id} | Up to limit entries strictly before the cursor. |
tail=true | The newest limit entries. |
afterCursor, beforeCursor, and tail=true are mutually exclusive. Invalid
combinations or an invalid/invisible beforeCursor return 400 Bad Request.
Channel, fork ancestry, epoch, upToEntryId, and fromSeq filters are applied
before the page is selected.
First page
curl "http://localhost:8080/v1/conversations/{conversationId}/entries?limit=2" \
-H "Authorization: Bearer <token>"
Response:
{
"data": [
{
"id": "aaa7b810-9dad-11d1-80b4-00c04fd430c8",
"conversationId": "d8c78c2b-b24a-46f4-86fd-22f066f74526",
"userId": "user_1234",
"channel": "history",
"contentType": "history",
"content": [{ "role": "USER", "text": "Hello!" }],
"createdAt": "2025-01-10T14:40:12Z"
},
{
"id": "bbb7b810-9dad-11d1-80b4-00c04fd430c9",
"conversationId": "d8c78c2b-b24a-46f4-86fd-22f066f74526",
"userId": "user_1234",
"channel": "history",
"contentType": "history",
"content": [{ "role": "AI", "text": "Hi there! How can I help?" }],
"createdAt": "2025-01-10T14:40:15Z"
}
],
"afterCursor": "bbb7b810-9dad-11d1-80b4-00c04fd430c9",
"beforeCursor": null
}
Next page
curl "http://localhost:8080/v1/conversations/{conversationId}/entries?limit=2&afterCursor=bbb7b810-9dad-11d1-80b4-00c04fd430c9" \
-H "Authorization: Bearer <token>"
Last page
To get the last page directly—without walking every forward page—set
tail=true. The limit parameter controls the maximum number of entries in
that last page. Results are still returned in chronological order.
curl "http://localhost:8080/v1/conversations/{conversationId}/entries?tail=true&limit=2" \
-H "Authorization: Bearer <token>"
The last-page response has afterCursor: null. When older entries exist,
beforeCursor is the ID of the first returned entry:
{
"data": [
{
"id": "ddd7b810-9dad-11d1-80b4-00c04fd430cb",
"content": [{ "role": "AI", "text": "Let me check that." }]
},
{
"id": "eee7b810-9dad-11d1-80b4-00c04fd430cc",
"content": [{ "role": "AI", "text": "Here is what I found." }]
}
],
"afterCursor": null,
"beforeCursor": "ddd7b810-9dad-11d1-80b4-00c04fd430cb"
}
Previous page
Pass the last page’s beforeCursor as the beforeCursor parameter to load the
adjacent older page:
curl "http://localhost:8080/v1/conversations/{conversationId}/entries?beforeCursor=ddd7b810-9dad-11d1-80b4-00c04fd430cb&limit=2" \
-H "Authorization: Bearer <token>"
Response:
{
"data": [
{
"id": "bbb7b810-9dad-11d1-80b4-00c04fd430c9",
"content": [{ "role": "AI", "text": "Hi there! How can I help?" }]
},
{
"id": "ccc7b810-9dad-11d1-80b4-00c04fd430ca",
"content": [{ "role": "USER", "text": "Can you look something up?" }]
}
],
"afterCursor": "ccc7b810-9dad-11d1-80b4-00c04fd430ca",
"beforeCursor": "bbb7b810-9dad-11d1-80b4-00c04fd430c9"
}
A middle page can contain both cursors: use beforeCursor to continue toward
older entries and afterCursor to return toward newer entries. Cursor anchors
are excluded, so adjacent pages do not duplicate their boundary entry.
Search Results
Search pagination works through the request body rather than query parameters. Pass afterCursor and limit in the JSON body of the POST request.
First page
curl -X POST "http://localhost:8080/v1/conversations/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"query": "memory service design",
"limit": 2
}'
Response:
{
"data": [
{
"conversationId": "d8c78c2b-b24a-46f4-86fd-22f066f74526",
"conversationTitle": "Design Discussion",
"entryId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"score": 0.93,
"highlights": "memory service design decisions",
"entry": { ... }
},
{
"conversationId": "660e8400-e29b-41d4-a716-446655440001",
"conversationTitle": "Architecture Review",
"entryId": "7ca7b810-9dad-11d1-80b4-00c04fd430d9",
"score": 0.87,
"highlights": "designing the memory layer",
"entry": { ... }
}
],
"afterCursor": "eyJzY29yZSI6MC44Nywi..."
}
Next page
curl -X POST "http://localhost:8080/v1/conversations/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"query": "memory service design",
"limit": 2,
"afterCursor": "eyJzY29yZSI6MC44Nywi..."
}'
Limits Reference
| Constraint | Value |
|---|---|
Minimum limit | 1 |
Maximum limit | 1000 by default (server-configurable) |
| Default limit (conversations) | 20 |
| Default limit (entries) | 50 |
| Default limit (memberships) | 50 |
| Default limit (search) | 20 |
| Default limit (unindexed) | 100 |
| Default limit (transfers) | 50 |
If no limit is provided, the endpoint-specific default is used. Requesting a limit above the maximum or below 1 returns a validation error.
Best Practices
- Always check
afterCursor— don’t assume a fixed number of pages. When it’snull, you’ve reached the end. - Use reasonable page sizes — smaller pages (20–50) give faster individual responses; larger pages (100–200) reduce the number of round trips.
- Don’t store cursors long-term — cursors are opaque position markers. They may become invalid if the underlying data changes (e.g., a conversation is deleted).
- Paginate in loops — for batch processing, loop until
afterCursorisnullrather than guessing when to stop. - Use entry cursors directionally — on entry responses, follow
beforeCursortoward older history andafterCursortoward newer history.
Next Steps
- Learn about Conversations
- Learn about Entries
- Explore Indexing & Search