pagination: { limit, page }. limit is a request: each method applies its own maximum, and 1000 is a common one rather than a rule — tasks.task.list answers 50 whatever is asked. A page shorter than limit is therefore not proof the data ended.Overview
When you need to retrieve all records from list methods of REST API version 3 with maximum efficiency and are ready to store the complete dataset in memory, use CallListV3.make().
This method automatically handles pagination and returns all data in a single array.
FetchList - it returns an asynchronous generator for step-by-step processing of large lists.// Basic usage
const response = await $b24.actions.v3.callList.make({
method: 'main.eventlog.list',
params: {
filter: [
['userId', '=', 1]
],
select: ['id', 'userId']
},
idKey: 'id',
customKeyForResult: 'items',
requestId: 'unique-request-id',
limit: 600
})
When to Use CallListV3.make()
- Small to medium data volumes: When the total number of records does not exceed several thousand.
- Simple processing: When you need simple access to all data at once.
Method Signature
make<T = unknown>(
options: ActionCallListV3
): Promise<Result<T[]>>
Parameters
The options object contains the following properties:
Return Value
Promise<Result<T[]>> — a promise that resolves to a Result<T[]> object containing an array of all retrieved elements.
This object provides:
.getData(): T[]— returns an array of all retrieved elements..isSuccess: boolean— flag indicating successful execution of all requests..getErrorMessages(): string[]— array of error messages.
Key Concepts
Performance Optimization
The method implements the Bitrix24 recommended algorithm for efficient work with large data volumes:
- Filtering by increasing id: Each subsequent query uses a
[cursorIdKey, '>', id]filter with the id of the last retrieved element (read viaidKey). - Automatic data end detection: Requests stop when an empty array is received, or when a page is shorter than the largest page seen so far. This keys the stop on the page size the server actually returns rather than the requested
options.limit, so a method that silently caps the page belowlimit(e.g.tasks.task.list, fixed at 50) still pages through every record instead of stopping after the first capped page. A short page ends the walk only once the cursor read from it has been checked — a page that is short and repeats the cursor is a server ignoring the page condition, not the end of the data, and is reported rather than returned.
REST API v3 Response Structure
customKeyForResult parameter is retained for backward compatibility. In the future, after analyzing real usage, a decision will be made regarding its necessity.In REST API version 3, various methods return data in the same structures:
- Grouped array:
{ result: { items: [...] } }
The customKeyForResult parameter allows you to specify the key where the data is located in the response.
note.*) also return a nextCursor field in the response envelope alongside items — { result: { nextCursor, items: [...] } }. You do not need to read or pass it: CallListV3.make() paginates with its own idKey cursor (the injected [idField, '>', n] filter) and walks every page regardless of nextCursor. The field is informational; the SDK ignores it.Limitations
- Page size is per method, not a global 1000:
limitstates what you want; the method decides what it gives. Measured on an on-premise build,tasks.task.listreturns 50 however much is asked, and nothing in the response says it was capped. The walker keys end-of-data on the largest page the server actually returned, so it pages through everything regardless — a hand-rolled loop oncall.makedoes not. - Sorting is fixed: The method always sorts by
cursorIdKey(which defaults toidKey) ascending, because cursor pagination relies on[cursorIdKey, '>', nextId]filters to walk the dataset. A user-suppliedordervalue would break that invariant, so the declaredorderproperty isOmitted from the type — though the[key: string]: unknownindex signature it inherits means the compiler still accepts one — and any value passed at runtime is stripped with awarninglog entry. To narrow the result set, usefilterinstead. filtermust be the v3 array form:[['id', '>', 100]], or the output ofFilterV3.build(...). TherestApi:v2object dialect ({ '>id': 100 }) is accepted byTypeCallParamsV3for backward compatibility and works with a plaincall, but not here: cursor pagination appends[cursorIdKey, '>', nextId]to the same filter on every page, so an array is the only shape it can extend. Passing an object throwsSdkErrorwith codeJSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY. It used to be accepted and then failed mid-walk withfilter is not iterable.- A cursor that stops advancing is fatal: if the last
idKeyvalue on a page equals the one already filtered on, the>cursorIdKeycondition was dropped and the same page will keep arriving. A full page cannot end the walk on its own — the largest-page-seen stop never fires — and a short one is checked before it is taken for the end of the data, so it throwsSdkErrorwith codeJSSDK_ACTION_CURSOR_STALLEDinstead of collecting duplicates for ever. The usual cause isidKeynaming the response field whilecursorIdKeyis left to default to it:tasks.task.listreturns a lowercaseidbut filters on an uppercaseID, so it needs both (idKey: 'id',cursorIdKey: 'ID'). A cursor that moves the wrong way is caught too, and separately: a server alternating between two pages never repeats the value just sent, so the equality test above cannot see it — the walk is stopped withJSSDK_ACTION_CURSOR_WENT_BACKWARDSon the first backwards step. And neither check waits for a full page any more: a short page whose cursor did not advance used to end the walk as end-of-data, returning a truncated and overlapping result with no error at all. - Only for list methods: Intended only for methods that return data arrays.
Error Handling
Always check the result using isSuccess and handle errors:
// @check-ignore: top-level return in error-handling illustration; some.method is a placeholder, not a portal method
const response = await $b24.actions.v3.callList.make({
method: 'some.method',
params: { /* some_params */ },
idKey: 'id',
customKeyForResult: 'items',
requestId: 'unique-request-id',
limit: 600
})
if (!response.isSuccess) {
// Handling error
console.error(new Error(`Error: ${response.getErrorMessages().join('; ')}`))
return
}
// Working with a successful result
const data = response.getData()
Examples
Getting all Event Log Items with filtering
import { B24Hook, LoggerFactory, Text, SdkError, AjaxError } from '@bitrix24/b24jssdk'
type MainEventLogItem = {
id: number
userId: number
}
const devMode = !!(typeof import.meta !== 'undefined' && (import.meta.dev || import.meta.env?.DEV))
const $logger = LoggerFactory.createForBrowser('Example:AllMainEventLogItems', devMode)
const $b24 = B24Hook.fromWebhookUrl('https://your_domain.bitrix24.com/rest/1/webhook_code/')
async function getMainEventLogItemList(requestId: string): Promise<MainEventLogItem[]> {
const sixMonthAgo = new Date()
sixMonthAgo.setMonth((new Date()).getMonth() - 6)
sixMonthAgo.setHours(0, 0, 0)
const response = await $b24.actions.v3.callList.make<MainEventLogItem>({
method: 'main.eventlog.list',
params: {
filter: [
['timestampX', '>=', Text.toB24Format(sixMonthAgo)] // created at least 6 months ago
],
select: ['id', 'userId']
},
idKey: 'id',
customKeyForResult: 'items',
requestId,
limit: 60
})
if (!response.isSuccess) {
throw new SdkError({
code: 'MY_APP_GET_PROBLEM',
description: `Problem ${response.getErrorMessages().join('; ')}`,
status: 404
})
}
return response.getData() ?? []
}
// Usage
const requestId = 'some-main-event-log-item-list'
try {
const list = await getMainEventLogItemList(requestId)
$logger.info(`List [${list?.length}]`, { requestId, list })
} catch (error) {
if (error instanceof AjaxError) {
$logger.critical(error.message, { requestId, code: error.code })
} else {
$logger.alert('Problem', { requestId, error })
}
}
When the request and response id fields differ
Some methods sort / filter by one field name but return another (for example an uppercase ID in the request vs a lowercase id in the payload). Set idKey to the response field and cursorIdKey to the request field:
// @check-ignore: some.list is a placeholder, not a portal method
const response = await $b24.actions.v3.callList.make({
method: 'some.list',
params: { select: ['ID', 'TITLE'] },
idKey: 'id', // read the id from each returned item
cursorIdKey: 'ID', // order + ["ID", ">", n] page filter in the request
customKeyForResult: 'items'
})
The two ways of getting this wrong fail differently, and the difference matters:
idKeycannot be read from a full page — you named a field the response does not carry. The SDK logs awarningand stops paginating rather than silently truncating; theResultholds what arrived so far, and it is real data.cursorIdKeynames a field the server does not filter on —idKeyreads fine, so the walk keeps going, but the page condition is dropped and the same page comes back for ever. That one throwsSdkErrorwith codeJSSDK_ACTION_CURSOR_STALLED, because everything collected at that point is one page repeated.
Alternatives and Recommendations
- For working with lists: Instead of manually managing pagination use:
FetchList— returns an async generator for step-by-step processing of large lists.
- For batch operations: Use
Batchto execute up to 50 commands in a single request. - On the client-side (browser): Use the built-in
B24Frameobject. - See also: Filtering — building the
filter(and whyorderis stripped).
Bounding the walk
The walk pages until the data runs out. Three options bound it.
maxPages stops after that many pages and throws JSSDK_ACTION_MAX_PAGES_EXCEEDED, naming the method. The rows already read are not discarded — they come back with the error attached, so check isSuccess. It defaults to 10 000 — a backstop rather than a policy: on restApi:v3 that is 500 000 rows at the default page size of 50, and more only where the method's own cap allows a larger limit, and at the default drain rate of 2 requests/second roughly 83 minutes, so a walk that never ends is bounded without capping a read anyone performs. Raise it when a read genuinely needs more.
It errors rather than truncating. A short list that looks complete is the failure mode this whole class of bug keeps producing, so nothing is returned.
signal takes an AbortSignal and throws JSSDK_ACTION_ABORTED. It is checked at the top of each iteration, so an already-aborted signal costs no request at all.
progress is called after each collected page with { pages, rows }. Counts, not a percentage: cursor paging reads no total — restApi:v3 sends none, and on v2 the walk never asks for one — so a denominator would have to be invented.
// const $b24 = ...
const controller = new AbortController()
const response = await $b24.actions.v3.callList.make({
method: 'main.eventlog.list',
params: { select: ['id', 'severity'] },
idKey: 'id',
customKeyForResult: 'items',
maxPages: 200,
signal: controller.signal,
progress: ({ pages, rows }) => console.log(`${pages} pages, ${rows} rows`)
})