Sessions & State
Pagination
Built-in pagination for list commands
Pagination#
For commands that return lists of items, Surf provides built-in pagination with a standard response envelope. Agents can detect paginated commands in the manifest and iterate automatically.
Declaring a Paginated Command
Set paginated: true for defaults, or pass a config object to customize limits and pagination style.
const surf = await createSurf({ name: 'My Store', commands: { 'products.list': { description: 'List all products', paginated: true, // Defaults: { defaultLimit: 20, maxLimit: 100, style: 'cursor' } params: { category: { type: 'string' }, }, run: async ({ category, cursor, limit }) => { const products = await db.products.list({ category, cursor, limit }) return paginatedResult(products.items, { nextCursor: products.nextCursor, total: products.total, }) }, },ย 'orders.history': { description: 'Get order history', auth: 'required', paginated: { defaultLimit: 10, maxLimit: 50, style: 'cursor', }, run: async ({ cursor, limit }, ctx) => { const orders = await db.orders.forUser(ctx.claims!.userId, { cursor, limit }) return paginatedResult(orders.items, { nextCursor: orders.nextCursor, total: orders.total, }) }, }, },})When paginated is set, Surf automatically injects cursor, limit, and offset into the command's accepted params. The limit is clamped to maxLimit and defaults to defaultLimit.
The paginatedResult Helper
Use paginatedResult() from @surfjs/core to build the standard response envelope. It derives hasMore from nextCursor automatically.
import { paginatedResult } from '@surfjs/core'ย // Basic usage โ hasMore is derived from nextCursorpaginatedResult(items, { nextCursor: 'abc123' })// => { items: [...], nextCursor: 'abc123', hasMore: true }ย paginatedResult(items, { nextCursor: null })// => { items: [...], nextCursor: null, hasMore: false }ย // With total countpaginatedResult(items, { nextCursor: 'abc123', total: 142 })// => { items: [...], nextCursor: 'abc123', hasMore: true, total: 142 }ย // Explicit hasMore overridepaginatedResult(items, { hasMore: false })Response Envelope
Paginated commands return a standard envelope that agents can rely on:
{ "ok": true, "result": { "items": [ { "id": "prod_1", "name": "Laptop", "price": 999 }, { "id": "prod_2", "name": "Mouse", "price": 29 } ], "nextCursor": "eyJpZCI6InByb2RfMiJ9", "hasMore": true, "total": 142 }}Client-Side Iteration
Agents iterate by passing the nextCursor from each response as the cursor param of the next request:
import { SurfClient } from '@surfjs/client'ย const client = await SurfClient.discover('https://shop.example.com')ย // Iterate through all pageslet cursor: string | undefinedconst allProducts = []ย do { const page = await client.execute('products.list', { category: 'electronics', limit: 20, cursor, })ย allProducts.push(...page.items) cursor = page.nextCursor ?? undefined} while (cursor)ย console.log(`Fetched ${allProducts.length} products`)Cursor vs Offset Style
Surf supports two pagination styles. Cursor-based is the default and recommended approach โ it's stable under concurrent inserts and deletes. Offset-based is available for simpler use cases.
// Cursor-based (default) โ stable, recommended{ paginated: { style: 'cursor', defaultLimit: 20 }}// Client sends: { cursor: "eyJ...", limit: 20 }ย // Offset-based โ simpler, but can skip/duplicate items on mutations{ paginated: { style: 'offset', defaultLimit: 20, maxLimit: 100 }}// Client sends: { offset: 40, limit: 20 }โ ๏ธ Note: Offset-based pagination can produce inconsistent results when items are inserted or deleted between requests. Prefer cursor-based pagination for production APIs.