v3.0.0

DeferredBatchV3

Deferred (background) batch for Bitrix24 REST API version 3: send thousands of commands in one call, follow the job status, and get the decoded rows — in one call with make(), or step by step.

Overview

$b24.actions.v3.deferredBatch wraps the portal's rest.deferredbatch.* methods. A deferred batch is a background job:

  1. rest.deferredbatch.add takes all commands in one call — thousands of them, far past the 50 of a synchronous batch. Measured: 5000 tasks.task.get commands in one job, finished in under 5 seconds (#570).
  2. The portal runs them in the background. The job's status goes pending → processing → done, or ends in error (all four observed).
  3. The results are stored as a gzip-compressed JSON file. The SDK downloads it and decodes it into rows — one row per command, in command order.

Compared with batchByChunk, which sends one request per 50 commands, a deferred batch is one request to start, then status checks until it is done. How the portal counts a background job against the operating-time limit has not been measured.

Deferred batches depend on the portal's plan. On a plan without them every call answers FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN (observed on such a portal, #570). Which scope the methods need has not been measured.
Rules measured on the portal (#570):
  • v3 methods only. A v2 method (user.current) or an unknown one is refused at add with INVALID_METHOD; no job is created.
  • All or nothing. If one command fails — a get of a record that does not exist is enough — the whole job ends in error (errorMessage: "Result does not exists") and there is no result file, not even for the commands that worked. Build commands that cannot fail on data: list pages rather than a get per id.
  • Duration depends on the method, not only the count. 5000 cheap gets finished in seconds; 500 rest.scope.list were still running after 15 minutes. Set timeout for your workload.
  • A running job cannot be deleted — the portal answers BATCH_PROCESSING. Delete it once it is done or error.
Measured through a webhook. In a browser (B24Frame) the result file can be read: the portal serves it with Access-Control-Allow-Origin: * (measured, #570); a full run from a frame has not been done. idempotencyKey is not available in a browser — see Idempotency-Key.

There are two ways to use it:

You want to…Use
Run the commands and get the rows, with a progress callbackmake()
Start a job now and collect it later — another request, a queue worker, your own UI pollingthe single steps

Method Signature

make<T = unknown>(options: ActionDeferredBatchV3): Promise<Result<T[]>>

add(options: ActionDeferredBatchAddV3): Promise<Result<DeferredBatchJob>>
get(id: number): Promise<Result<DeferredBatchJob>>
waitFor(id: number, options?: ActionDeferredBatchWaitV3): Promise<Result<DeferredBatchJob>>
download<T = unknown>(id: number): Promise<Result<T[]>>
delete(id: number): Promise<Result<boolean>>
list(): Promise<Result<DeferredBatchJob[]>>
getDownloadUrl(id: number): Promise<Result<string>>
decode(bytes: ArrayBuffer | Uint8Array): Promise<unknown[]>

All in one: make()

// const $b24 = ...
import type { BatchCommandsArrayUniversal, DeferredBatchJob } from '@bitrix24/b24jssdk'

interface TaskPage { items: Array<{ id: number, title: string }> }

// 200 pages of 50 tasks. List pages, not a `get` per id: one failing command
// fails the whole job, while a page past the end is just empty (measured).
const calls: BatchCommandsArrayUniversal = Array.from({ length: 200 }, (_, i) => [
  'tasks.task.list',
  { select: ['id', 'title'], order: { id: 'ASC' }, pagination: { page: i + 1, limit: 50 } }
])

const response = await $b24.actions.v3.deferredBatch.make<TaskPage>({
  calls,
  // Called on every status change, e.g. pending → done.
  onStatus: (job: DeferredBatchJob) => console.log(`job #${job.id}: ${job.status}`)
})

if (!response.isSuccess) {
  throw new Error(response.getErrorMessages().join('; '))
}

const pages = response.getData()! // one row per command, in order
const titles = pages.flatMap(page => page.items.map(task => task.title))
console.log(`${titles.length} tasks in ${pages.length} pages`)

make() adds the job, waits for it, downloads and decodes the result file, and deletes the job. What the portal answers never makes it throw: check isSuccess. It throws only for malformed calls, before anything is sent.

Options

OptionTypeDefaultMeaning
callsDeferredBatchCalls—The commands: [['method', params], …], [{ method, params }, …], or both mixed. Named commands are not supported — the result is an array.
onStatus(job: DeferredBatchJob) => void—Called when the status changes, the first status included. An exception in it — or a rejection, if it is async — is logged and ignored.
pollIntervalnumber2000Milliseconds between status checks. At least 250.
timeoutnumber600000Milliseconds to wait for done / error.
signalAbortSignal—Stops waiting.
deleteAfterbooleantrueDelete the job and its file after reading.
idempotencyKeystring—Sent with add. Use one key per run: by the portal's idempotency contract — not measured for deferred batches — a retry of an interrupted make() with the same key then does not start a second job, while a key reused after a finished make() deleted its job would replay the id of a job that no longer exists. See Idempotency-Key.
requestIdstring—Sent as bx24_request_id with add.

Examples

Progress in a UI, and cancelling

// const $b24 = ...
declare const calls: [string, Record<string, unknown>][]
declare function showStatus(text: string): void

const controller = new AbortController()
// e.g. wired to a "Cancel" button: cancelButton.onclick = () => controller.abort()

const response = await $b24.actions.v3.deferredBatch.make({
  calls,
  pollInterval: 3_000,
  timeout: 30 * 60_000,
  signal: controller.signal,
  onStatus: job => showStatus(job.status === 'done' ? 'Downloading results…' : `Status: ${job.status}`)
})

if (!response.isSuccess) {
  // JSSDK_DEFERRED_BATCH_ABORTED, _TIMEOUT or _FAILED — see "Errors" below.
  showStatus(response.getErrorMessages().join('; '))
}

Aborting or timing out stops waiting, not the job: it keeps running on the portal, and can be collected later with the steps below. make()'s Result does not carry the job id; onStatus does — it is called with the job as soon as it is first read — so keep the id from there if you may want to collect it.

Step by step

Each step is one method and returns a Result:

MethodPortal methodReturns
add({ calls, idempotencyKey?, requestId? })rest.deferredbatch.addResult<DeferredBatchJob> — the new job, usually pending
get(id)rest.deferredbatch.getResult<DeferredBatchJob>
waitFor(id, { pollInterval?, timeout?, signal?, onStatus? })rest.deferredbatch.get, repeatedResult<DeferredBatchJob> — the job in its final state
download<T>(id)rest.deferredbatch.downloadresult + the fileResult<T[]> — the decoded rows
delete(id)rest.deferredbatch.deleteResult<boolean> — refused while the job is processing
list()rest.deferredbatch.listResult<DeferredBatchJob[]>
getDownloadUrl(id)rest.deferredbatch.downloadresultResult<string> — the file link (a credential, see below)
decode(bytes)—Promise<unknown[]> — gunzip + JSON of file bytes

Start now, collect later

// const $b24 = ...
declare const calls: [string, Record<string, unknown>][]
declare function saveJobId(id: number): Promise<void>
declare function loadJobId(): Promise<number>

// Request 1: start the job and remember its id.
const added = await $b24.actions.v3.deferredBatch.add({ calls })
if (!added.isSuccess) {
  throw new Error(added.getErrorMessages().join('; '))
}
await saveJobId(added.getData()!.id)

// Request 2, later: is it ready?
const jobId = await loadJobId()
const job = (await $b24.actions.v3.deferredBatch.get(jobId)).getData()

if (job?.status === 'done') {
  const rows = await $b24.actions.v3.deferredBatch.download(jobId)
  console.log(`${rows.getData()?.length ?? 0} rows`)
  await $b24.actions.v3.deferredBatch.delete(jobId)
} else if (job?.status === 'error') {
  console.error(job.errorMessage)
}

The job object

interface DeferredBatchJob {
  id: number
  status: 'pending' | 'processing' | 'done' | 'error'
  commands?: Array<{ method: string, query: Record<string, unknown> }>
  createdAt?: string
  updatedAt?: string
  resultFileId?: number | null // set once `done`
  errorMessage?: string | null // set when `error`
}

The result rows

A row is what that command returned, in command order — the same shape as one entry of a synchronous v3 batch result: { item } for a get, { items } for a list, and so on. The row carries no time block. There are no error rows: a job with a failing command has no result file at all (see the rules above).

The whole file is held in memory while it is decoded. Rows that each carry a full list page add up quickly; split very large exports into several jobs.

download() / make() decode the file for you. If you downloaded it some other way, decode() does the same: it gunzips the bytes (the file is application/gzip) and parses the JSON array. Bytes already inflated — a proxy that decompressed them — are parsed as they are. Decoding uses the standard DecompressionStream, available in Node.js 18+ and current browsers.

rest.deferredbatch.downloadresult answers with an absolute URL that carries the caller's credential — measured: the webhook secret in the path; per the portal's code, an access token for an OAuth app. The SDK downloads it itself, keeps it out of the errors it builds, refuses redirects with it, and its log of the downloadresult answer masks the secret and the token. getDownloadUrl() hands it to you: treat it like the webhook URL itself. Deleting the job makes the link stop working.

Error Handling

CodeWhen
JSSDK_DEFERRED_BATCH_FAILEDThe job ended with status error — one failing command is enough. make() logs the portal's errorMessage and deletes the job (unless deleteAfter: false); waitFor() returns the job as data, with its errorMessage. The message names the job id.
JSSDK_DEFERRED_BATCH_TIMEOUTNo final status within timeout. The job keeps running and cannot be deleted yet; the message names its id, to collect it later.
JSSDK_DEFERRED_BATCH_ABORTEDsignal was aborted. A job already added keeps running; the message names its id.
JSSDK_DEFERRED_BATCH_DOWNLOAD_FAILEDThe result file could not be downloaded; the message names the HTTP status.
JSSDK_DEFERRED_BATCH_DECODE_FAILEDThe file is not valid gzip, or not a JSON array.
JSSDK_DEFERRED_BATCH_GZIP_UNSUPPORTEDThe runtime has no DecompressionStream.
JSSDK_DEFERRED_BATCH_EMPTYcalls is empty — thrown, before anything is sent.
JSSDK_DEFERRED_BATCH_UNEXPECTED_RESPONSEThe portal answered without the data the step expects (e.g. add without an item).
JSSDK_INTERACTION_BATCH_ROW_FAILA command in calls is neither a tuple nor a { method, params } object — thrown, before anything is sent.
FEATURE_NOT_AVAILABLE_ON_CURRENT_PLANThe portal's plan has no deferred batches.
INVALID_METHODadd named a v2 or unknown method; a deferred batch takes v3 methods only. No job was created.
BATCH_PROCESSINGdelete of a job that is still processing.

Alternatives and Recommendations

  • Up to 50 commands, result needed right away: a synchronous batch is simpler and returns in the same request.
  • More than 50, and the plan has no deferred batches:batchByChunk does the same work as one request per 50 commands.
  • Reading a whole list: fetchList or fetchTail page through it without building thousands of get commands.
  • Writes that must not repeat: pass idempotencyKey, one per run, so a retried add does not start a second job (the portal's idempotency contract; not measured for deferred batches).
  • Cancelling: signal works as on the list actions, but an abort comes back as JSSDK_DEFERRED_BATCH_ABORTED on the Result rather than a thrown JSSDK_ACTION_ABORTED, in line with make() never throwing.
  • Large result files: the download uses the HTTP client's timeout (30 s by default), not timeout. Raise getHttpClient(ApiVersion.v3).ajaxClient.defaults.timeout for very large jobs.
  • Writes: the job is all-or-nothing only for its result. A failing command ends it in error without a file, but nothing says the writes that ran before it were undone — so for writes, prefer commands that cannot fail, and check the portal's data after an error.