A typical batch / cron-style job: export companies into a CSV with one webhook. Stays under the rate limits and uses fetchList so memory stays flat regardless of dataset size.
Project Layout
my-export-job/
├── dist/
├── src/
│ └── process-company-list.ts
├── out/ # generated *.csv files
├── .env.local # B24_HOOK=...
├── package.json
└── tsconfig.json
package.json
{
"name": "my-export-job",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node --env-file=.env.local dist/process-company-list.js"
},
"devDependencies": {
"@types/node": "^22",
"typescript": "^5.6"
},
"dependencies": {
"@bitrix24/b24jssdk": "latest",
"chalk": "^5"
}
}
src/process-company-list.ts
// @check-ignore: node:fs not available in docs typecheck context
import { writeFileSync } from 'node:fs'
import {
B24Hook,
EnumCrmEntityTypeId,
LoggerFactory,
Text,
type ISODate
} from '@bitrix24/b24jssdk'
const $logger = LoggerFactory.createForBrowser('export', process.env.NODE_ENV === 'development')
if (!process.env.B24_HOOK) {
$logger.error('B24_HOOK is not set in .env.local')
process.exit(1)
}
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
$logger.info(`Portal: ${$b24.getTargetOrigin()}`)
type Company = { id: number, title: string, createdTime: ISODate }
async function main() {
const generator = $b24.actions.v2.fetchList.make<Company>({
method: 'crm.item.list',
params: {
entityTypeId: EnumCrmEntityTypeId.company,
select: ['id', 'title', 'createdTime']
},
idKey: 'id',
requestId: 'export:companies'
})
const rows: string[] = ['id,title,createdTime']
for await (const chunk of generator) {
for (const c of chunk) {
rows.push([
c.id,
JSON.stringify(c.title ?? ''),
Text.toDateTime(c.createdTime).toFormat('yyyy-LL-dd HH:mm:ss')
].join(','))
}
$logger.info(`Loaded ${chunk.length} (total ${rows.length - 1})`)
}
const out = `out/companies-${Date.now()}.csv`
writeFileSync(out, rows.join('\n'), 'utf8')
$logger.notice(`Wrote ${out}`)
$b24.destroy()
}
main().catch((error) => {
$logger.error('export failed', { error })
process.exit(1)
})
Why fetchList?
- Memory-flat: each
for awaititeration yields a 50-item chunk, so a 50 000-row dataset never materialises in RAM. - Built-in pagination: don't roll your own
start: nextStartloop. - Gracefully handles the SDK rate / operating limiters between pages.
For full-blown bulk operations (delete-then-create, bulk imports), pair the export with actions.v2.batchByChunk.