Overview
$b24.actions.v3.deferredBatch wraps the portal's rest.deferredbatch.*
methods. A deferred batch is a background job:
rest.deferredbatch.addtakes all commands in one call — thousands of them, far past the 50 of a synchronousbatch. Measured: 5000tasks.task.getcommands in one job, finished in under 5 seconds (#570).- The portal runs them in the background. The job's status goes
pending→processing→done, or ends inerror(all four observed). - 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.
FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN (observed on such a portal,
#570). Which scope the methods
need has not been measured.- v3 methods only. A v2 method (
user.current) or an unknown one is refused ataddwithINVALID_METHOD; no job is created. - All or nothing. If one command fails — a
getof a record that does not exist is enough — the whole job ends inerror(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 agetper id. - Duration depends on the method, not only the count. 5000 cheap
gets finished in seconds; 500rest.scope.listwere still running after 15 minutes. Settimeoutfor your workload. - A running job cannot be deleted — the portal answers
BATCH_PROCESSING. Delete it once it isdoneorerror.
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:
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
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:
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.
Security: the download link is a credential
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
Alternatives and Recommendations
- Up to 50 commands, result needed right away: a synchronous
batchis simpler and returns in the same request. - More than 50, and the plan has no deferred batches:
batchByChunkdoes the same work as one request per 50 commands. - Reading a whole list:
fetchListorfetchTailpage through it without building thousands ofgetcommands. - Writes that must not repeat: pass
idempotencyKey, one per run, so a retriedadddoes not start a second job (the portal's idempotency contract; not measured for deferred batches). - Cancelling:
signalworks as on the list actions, but an abort comes back asJSSDK_DEFERRED_BATCH_ABORTEDon theResultrather than a thrownJSSDK_ACTION_ABORTED, in line withmake()never throwing. - Large result files: the download uses the HTTP client's timeout (30 s by
default), not
timeout. RaisegetHttpClient(ApiVersion.v3).ajaxClient.defaults.timeoutfor very large jobs. - Writes: the job is all-or-nothing only for its result. A failing
command ends it in
errorwithout 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 anerror.