Overview
CallTailV3.make() walks a restApi:v3 tail method — a method that pages by the server's own keyset cursor — and returns every record as one array.
main.eventlog.tail answered; the rest returned METHODNOTFOUNDEXCEPTION.Ask the portal rather than trusting that sample. rest.documentation.openapi returns every v3 method it publishes, keyed by path:const doc = await $b24.actions.v2.call.make<{ paths: Record<string, unknown> }>({
method: 'rest.documentation.openapi',
params: {}
})
const tails = Object.keys(doc.getData()?.result?.paths ?? {})
.filter(path => path.endsWith('.tail'))
CallList, which reaches the same result by emulating a cursor on an ordinary list method.// Basic usage
const response = await $b24.actions.v3.callTail.make({
method: 'main.eventlog.tail',
params: {
select: ['id', 'auditTypeId']
},
cursorField: 'id',
customKeyForResult: 'items'
})
How it differs from CallList
The two walkers reach the same place by different roads, and the difference decides what filter may contain.
Because the list walkers have to append a condition to your filter, they can only accept the array form. The tail walkers add nothing, so a bare FilterV3.or(...) group is a legal whole filter here.
When to Use CallTailV3.make()
- The method publishes a
tailendpoint and you want the server's own cursor rather than an emulated one. - Small to medium data volumes, held in memory at once. For a large export use
FetchTail, which yields page by page.
Method Signature
make<T = unknown>(
options: ActionCallTailV3
): Promise<Result<T[]>>
Parameters
Return Value
Promise<Result<T[]>> — resolves to a Result<T[]> holding every row read.
.getData(): T[] | null | undefined— the rows. Nullable by type, which is why the examples below reach for?.length ?? 0; a completed walk always sets an array, even an empty one..isSuccess: boolean— false when a bound was hit or the portal returned a soft error mid-walk; the rows already read are still there. Not every failure arrives this way — see Error Handling..getErrorMessages(): string[]— the messages.
Key Concepts
Choosing a cursor field
The walk asks for rows strictly past the last value it saw, so the field has to carry the whole ordering by itself. Three requirements:
- Unique. The condition is strictly greater, so rows sharing the last row's value across a page boundary are never asked for — this follows from the condition rather than from a measurement.
idis the safe default for a reason. - Scalar. A number or a string. The walk reads the value straight off the response, so an object- or array-valued field cannot serve as a cursor.
- Readable. The value has to come back, or there is nothing to advance by. The SDK appends the field to an explicit
selectfor you; with noselectand a non-default field it warns instead, because it cannot know the server's default field set.
warning-level log line, and only when the page was full. So if a walk returns a suspiciously round number of rows — exactly one page, say — check cursorField before you believe it.For a non-numeric field, pass initialValue as well: the default is the number 0, and the SDK does not check this.
The cursor field must not appear in filter
The server orders and pages by that field, and refuses a filter on it. Measured on main.eventlog.tail:
{ "error": { "code": "BITRIX_REST_V3_EXCEPTION_INVALIDFILTEREXCEPTION",
"message": "Cursor field id cannot be used…" } }
The SDK warns before the request rather than letting you find out from the portal:
callTail.make: the cursor field "id" must not appear in `filter` — the server
orders and pages by it and will reject a filter on the same field
(INVALIDFILTEREXCEPTION). Remove it from `filter`.
The check descends into logic groups, so a field buried inside FilterV3.or(...) is found too.
The cursor field is added to select
The walk advances by reading cursorField off the last row, so the field has to come back. If you pass select, the SDK appends the cursor field when it is missing. If you pass no select at all and the cursor field is not the default id, it warns instead — it cannot know the server's default field set.
order: 'DESC' requires initialValue
A descending walk pages by field < value, so the default first value of 0 would match nothing. The SDK refuses it at call time rather than returning a silently empty result:
// Throws JSSDK_CORE_B24_CALL_TAIL_DESC_REQUIRES_INITIAL_VALUE
await $b24.actions.v3.callTail.make({
method: 'main.eventlog.tail',
cursorField: 'id',
order: 'DESC',
customKeyForResult: 'items'
})
Pass a value that sorts after every row. For a numeric field that is the type maximum; for a datetime field, a far-future ISO-8601 timestamp stating a zone (the backwards-cursor guard only compares datetimes that state the same zone, so a mismatch quietly turns the guard off); for any other string, a value that sorts last.
const response = await $b24.actions.v3.callTail.make({
method: 'main.eventlog.tail',
cursorField: 'id',
order: 'DESC',
initialValue: Number.MAX_SAFE_INTEGER,
customKeyForResult: 'items'
})
Error Handling
Two of these come back, five reject. Only the last two are bounds you set, so the rows already collected are correct and are handed back with the error attached. The other four — a refused filter, a missing initialValue, a stalled or backwards cursor — reject the promise, because there is no result worth keeping. So isSuccess alone is not enough:
try {
const response = await $b24.actions.v3.callTail.make({
method: 'main.eventlog.tail',
cursorField: 'id',
customKeyForResult: 'items'
})
// Reached the ceiling or was aborted: the rows are here and are correct.
if (!response.isSuccess) {
console.warn(response.getErrorMessages(), response.getData()?.length ?? 0)
}
}
catch (error) {
// Filter refused, a bad `maxPages`, DESC without `initialValue`, or the
// cursor stopped advancing.
console.error(error)
}
A soft error from the portal mid-walk also lands on the Result rather than throwing, so isSuccess covers that case too.
See Error codes and handling for the full table.
Pointing it at a list method
A list method does not implement the cursor, and — measured on main.eventlog.list — the portal ignores the cursor parameter silently rather than refusing it. On that method an unknown top-level key was ignored the same way, so nothing in the response says the walk is not paging. Whether every v3 list method behaves like that was not measured; one that does is enough to matter, because the symptom is indistinguishable from a working walk until the pages start repeating.
Before the cursor guards existed, that walk repeated its first page for ever. Now it stops:
// Throws JSSDK_ACTION_CURSOR_STALLED — main.eventlog.list has no tail
await $b24.actions.v3.callTail.make({
method: 'main.eventlog.list',
cursorField: 'id',
customKeyForResult: 'items'
})
If you meant to walk a list method, use CallList.
Examples
Reading an event log, oldest first
const response = await $b24.actions.v3.callTail.make<{ id: number }>({
method: 'main.eventlog.tail',
params: { select: ['id', 'auditTypeId'] },
cursorField: 'id',
customKeyForResult: 'items',
limit: 100
})
if (!response.isSuccess) {
console.warn(response.getErrorMessages())
}
console.log(`rows: ${response.getData()?.length ?? 0}`)
Filtering with a bare logic group
The list walkers require the array form because they extend it. Tail forwards the filter untouched, so a group on its own is accepted:
import { FilterV3 } from '@bitrix24/b24jssdk'
const response = await $b24.actions.v3.callTail.make({
method: 'main.eventlog.tail',
params: {
filter: FilterV3.or(
['auditTypeId', '=', 'MAIN_EVENTLOG'],
['auditTypeId', '=', 'MAIN_LOGIN']
),
select: ['id', 'auditTypeId']
},
cursorField: 'id',
customKeyForResult: 'items'
})
A complete script
import { B24Hook, SdkError } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl('https://your-portal.bitrix24.com/rest/1/SECRET')
try {
const response = await $b24.actions.v3.callTail.make<{ id: number }>({
method: 'main.eventlog.tail',
params: { select: ['id', 'auditTypeId'] },
cursorField: 'id',
customKeyForResult: 'items'
})
if (!response.isSuccess) {
console.warn(response.getErrorMessages())
}
console.log(`rows: ${response.getData()?.length ?? 0}`)
}
catch (error) {
if (error instanceof SdkError) {
console.error(`the walk could not run: ${error.code}`)
}
throw error
}
An empty result is an ordinary answer, not a failure: isSuccess is true and the array is empty. Note that a wrong customKeyForResult looks exactly the same — the walker treats a key it cannot find as "no data" — so a silently empty walk is worth checking the key for first.