# Pagination and batching

Follow continuations to finish a read, and batch independent reads in one request.

Last updated 8 November 2026

A query can return part of a result with a `more` continuation. Send that continuation back unchanged to get the next part, and repeat until `more` is `null`. Batching lets you send several independent reads together; each result can still need its own continuation.

## Read every message page

This example uses the token and workspace ID from [First API request](https://cabane.ai/docs/first-api-request). Start there if you have not set `CABANE_TOKEN` and `CABANE_WORKSPACE_ID` yet.

In the same Bash session, export those existing variables so Node can read them through `process.env`. Then enter the ID of the conversation you want to read:

~~~bash
export CABANE_TOKEN CABANE_WORKSPACE_ID
read -r -p "Conversation ID: " CABANE_CONVERSATION_ID
export CABANE_CONVERSATION_ID
~~~

The `export` command passes the existing token to Node without putting its value in command history.

Save the following as `read-messages.mjs`. It requires Node.js 20 or later and no extra packages.

~~~js
const required = [
  'CABANE_TOKEN',
  'CABANE_WORKSPACE_ID',
  'CABANE_CONVERSATION_ID',
];
for (const name of required) {
  if (!process.env[name]) {
    throw new Error(`Set and export ${name} before running this script.`);
  }
}

const token = process.env.CABANE_TOKEN;
const workspaceId = process.env.CABANE_WORKSPACE_ID;
const conversationId = process.env.CABANE_CONVERSATION_ID;
const endpoint =
  `https://app.cabane.ai/api/workspaces/${encodeURIComponent(workspaceId)}/ops/query`;

let query = {
  op: 'conversations.listMessages',
  args: { id: conversationId, order: 'asc', limit: 20 },
};

while (query !== null) {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(query),
    signal: AbortSignal.timeout(30_000),
  });

  if (!response.ok) {
    throw new Error(`Query failed with HTTP ${response.status}.`);
  }
  const envelope = await response.json();
  if (envelope.ok !== true) {
    throw new Error(`Query failed: ${JSON.stringify(envelope.error)}`);
  }

  for (const message of envelope.result.messages) {
    console.log(JSON.stringify(message));
  }
  query = envelope.result.more;
}
~~~

Run it in the shell where you exported the variables:

~~~bash
node read-messages.mjs
~~~

The script prints one message row per line in ascending order and stops when the page's `more` is `null`. It checks both the HTTP status and the response envelope's `ok` value. Each request has a 30-second timeout.

The next request is exactly `envelope.result.more`: an object containing `op` and `args`. Keep both unchanged, and send them to the same query endpoint. Do not build a cursor or copy the first request's arguments over the continuation.

## Finish shortened rows too

The loop above finishes the message pages. It does not automatically retrieve shortened content inside a message row.

A final page can still contain rows with their own `more`. If you need a row's full content, send that row's continuation as a separate query and follow any further continuations it returns. A row with no continuation is complete.

A row continuation can name a different operation and carry offsets or version information. Treat it as the next request, unchanged, just like a page continuation. Finishing a page's continuation and finishing its rows are separate jobs.

## Choose the response size

Put `maxBytes` inside an operation's `args` to set its output budget. The default is 16 KiB (16384 bytes); the maximum is 23 KiB (23552 bytes). Use `limit` only where the operation declares it, as `conversations.listMessages` does above. See the [Operation reference](https://cabane.ai/docs/api) for each operation's arguments and result.

Some bounded results, including `files.list`, report `truncated` instead of providing `more`. If a result is truncated, narrow the request. There is no continuation to follow for that result.

## Batch independent reads

Send up to 16 independent read operations in an `ops` array to the same `POST /api/workspaces/{id}/ops/query` endpoint, with the same bearer token and JSON headers. For example, this request reads rooms and agents:

~~~json
{
  "ops": [
    { "op": "rooms.list", "args": { "maxBytes": 4096 } },
    { "op": "agents.list", "args": { "maxBytes": 4096 } }
  ]
}
~~~

Check the HTTP status and outer envelope's `ok`, then inspect every entry in `results`. Entries appear in input order and identify their `op`. Each has either `ok: true` with a `result`, or `ok: false` with an `error`. A successful outer envelope does not mean every operation succeeded. Follow any continuations in successful results as you would for a single read.

The batch shares one output budget. An entry with `error.kind: "output_too_large"` can have either of these codes:

| `error.code` | What happened | What to do |
| --- | --- | --- |
| `not_run` | The operation did not execute. | Resend the original read alone or in a smaller batch. |
| `not_returned` | The operation executed, but its answer did not fit. | Resend the original read alone or in a smaller batch. |

Neither error supplies a `more` continuation. Keep the original reads so you can resend the affected entries.

Writes cannot be batched. If one read needs a value returned by another, run them sequentially: finish the first read, then build the next request from its result.
