v3.0.0

CallTailV3.make

Method for retrieving all records of a REST API version 3 tail method through the server's native keyset cursor.

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.

Check that your method publishes a tail endpoint before you reach for this. It is a separate method from its list counterpart, and few modules have one — sampling ten plausible candidates on a cloud portal in September 2026, only 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'))
See Discovering v3 methods for what else that document carries.For everything else use 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.

callList / fetchListcallTail / fetchTail
PaginationEmulated — appends [cursorIdKey, '>', cursor] to your filterNative — sends cursor: { field, value, order, limit }, filter forwarded untouched
orderReserved by the walkerYours, through order: 'ASC' | 'DESC'
filter shapeThe array form only, because the walker has to extend itThe array form or a bare logic group
Works onAny list methodOnly a method that implements tail

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()

  1. The method publishes a tail endpoint and you want the server's own cursor rather than an emulated one.
  2. 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

ParameterTypeRequiredDescription
methodstringYesA restApi:v3 tail method, e.g. main.eventlog.tail. Pointing this at a .list method does not work — see Pointing it at a list method.
paramsOmit<TypeCallParamsV3, 'pagination' | 'order' | 'cursor' | 'filter'> & { filter?: TypeFilterV3 | FilterV3Group }NoRequest parameters. pagination, order and cursor are reserved — the walker drives the cursor and takes the direction from order below. filter accepts the restApi:v3 array form or a bare logic group.
cursorFieldstringNoThe field the server pages by. Default 'id'. It must be unique, totally ordered and scalar — see Choosing a cursor field. It must also be readable in the response: an explicit select gets it appended automatically, and omitting select with a non-default cursorField earns a warning.
order'ASC' | 'DESC'NoDirection of the walk. Default 'ASC'; lowercase is accepted too. 'DESC' requires initialValue — see below.
initialValuenumber | stringNoThe cursor value the first page starts from. Default 0, which suits an ascending numeric field. Required for 'DESC' — that one the SDK enforces. You also need it for any non-numeric field, and there the SDK does not check: the default 0 is a number and would be sent as-is.
customKeyForResultstringNoThe key the rows live under in the response envelope. Default 'items'.
requestIdstringNoIdentifier for tracing, sent as bx24_request_id. It deduplicates nothing.
limitnumberNoRows per page. Default 50. A request, not a guarantee — each method applies its own maximum, and a short page is not the end of the data.
maxPagesnumberNoCeiling on pages read. Default 10000; must be a positive integer, or JSSDK_ACTION_INVALID_MAX_PAGES is raised at call time. Reaching it does not discard what was read: the rows come back with JSSDK_ACTION_MAX_PAGES_EXCEEDED attached, so check isSuccess.
signalAbortSignalNoCancels the walk, checked before each request. The pages already read come back with JSSDK_ACTION_ABORTED attached.
progress(p: { pages: number, rows: number }) => voidNoCalled after each page with cumulative counts. Counts rather than a percentage: keyset paging never asks for a total.

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. id is 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 select for you; with no select and a non-default field it warns instead, because it cannot know the server's default field set.
Getting this wrong truncates the result silently. None of the three raises an error. A cursor field that is missing from the response, or is not a scalar, is treated as no cursor: the walk stops there and resolves successfully with the pages it already had. A non-unique field is worse still — it loses individual rows at page boundaries and nothing marks the gap.The only signal is a 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

CodeWhen
JSSDK_ACTION_V3_TAIL_FILTER_INVALIDfilter was the restApi:v2 object dialect, e.g. { '>id': 100 }. Refused here rather than one round trip later, where the portal reports it in wording that never names the dialect.
JSSDK_CORE_B24_CALL_TAIL_DESC_REQUIRES_INITIAL_VALUEorder: 'DESC' without initialValue.
JSSDK_ACTION_CURSOR_STALLEDThe cursor came back equal to the one just sent — the server is answering with the same page. On a tail walk the usual causes are a cursorField that is not the field the server pages by or whose values are not unique, and a method that does not implement tail at all.
JSSDK_ACTION_CURSOR_WENT_BACKWARDSThe cursor moved into a value the walk had already passed. With order: 'DESC', check that initialValue is the newest value rather than the oldest.
JSSDK_ACTION_MAX_PAGES_EXCEEDEDThe page ceiling was reached. The rows read are returned with the error attached — check isSuccess, do not assume a returned list is whole.
JSSDK_ACTION_INVALID_MAX_PAGESmaxPages was not a positive integer. Raised before any request.
JSSDK_ACTION_ABORTEDThe signal fired. Handled like the ceiling: what was read comes back with the error attached.

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.

Alternatives and Recommendations

  • FetchTail — the same walk as an async generator, for datasets too large to hold at once.
  • CallList — for the ordinary list methods, which is nearly all of them.
  • Filtering — the filter dialects and which walker accepts which.