Import & Export
Bulk-load CSV files into any registered entity type and extract records to downloadable files through per-plugin BullMQ workers.
For field definitions referenced by column mapping, see Properties, Tags & Segments.
Module layout
backend/core-api/src/modules/import-export/
graphql/schema/{import,export,common}.ts
graphql/resolvers/{queries,mutations}
db/definitions/{import,export}.ts, db/models/{Imports,Exports}.ts
trpc/{import,export,templates,index}.ts
utils/{getImportExportTypes,getRequiredPermissions,validateConfig}.ts
workers/utils/errorFileHandler.ts
routes.ts # GET /import-export/download-template
backend/core-api/src/meta/import-export/ # startImportExportWorker setup
backend/erxes-api-shared/src/core-modules/import-export/
worker.ts # BullMQ workers per plugin
utils/{importBatchProcess,exportBatchProcess,importUtils,
headerMatcher,tempWorkspace,...}.ts
Each entity type is identified as <plugin>:<module>.<collection> (for example core:contacts.customers, core:contacts.leads, core:contacts.companies, core:product.product, core:organization.users).
How a plugin registers types
Each service calls startImportExportWorker from its meta entry (core: meta/import-export/index.ts):
// backend/core-api/src/meta/import-export/index.ts
export default async (app: Express) =>
startImportExportWorker({
pluginName: 'core',
config: { import: importConfiguration, export: exportConfiguration },
app,
});
startImportExportWorker (worker.ts, idempotent per pluginName) starts two BullMQ workers on queues <plugin>-import-processor and <plugin>-export-processor, tunable with IMPORT_EXPORT_IMPORT_CONCURRENCY, IMPORT_EXPORT_EXPORT_CONCURRENCY, and IMPORT_EXPORT_*_LIMITER_MAX/_DURATION_MS env vars. The import/export configs map content types to module handlers; core dispatches to contacts, product, and organization handler sets (createCoreModuleProducerHandler):
- Import:
importHandlers+process*Rows.tsper entity (e.g.contacts/meta/import-export/import/customers/processCustomerRows.ts). - Export:
getExportHeaders,getExportData,build*ExportRowper entity.
Each registered type also lists its required permission actions (customersImportManage, companiesImportManage, productsImportManage, teamMembersImportManage, plus the *ExportManage twins), returned by importExportTypes.
GraphQL API
Imports
| Operation | Key arguments |
|---|---|
importColumnPreview | entityType!, fileKey!, fileName!; returns detected columns (header, key, confidence, status, sampleValues) and target fields |
importFields | entityType!; returns available target fields with required, type, options, example |
importStart | entityType!, fileKey!, fileName!, columnMapping: [{ index, header, key }] |
importProgress | importId!; returns status, progress, processedRows/totalRows, rowsPerSecond, estimatedSecondsRemaining |
activeImports | entityType |
importHistories | entityType, entityTypes, status, limit, cursor, direction, cursorMode |
importCancel / importRetry / importResume | importId! |
The Import record tracks status, totalRows, processedRows, successRows, errorRows, importedIds, errorFileUrl, progress, elapsedSeconds, rowsPerSecond, estimatedSecondsRemaining, jobId, userId, subdomain.
Exports
| Operation | Key arguments |
|---|---|
exportHeaders | entityType!, filters; returns [{ label, key, isDefault, type }] |
exportStart | entityType!, filters, ids, selectedFields |
exportHistories | entityTypes, status, limit, cursor, direction, cursorMode |
exportCancel / exportRetry | exportId! |
The Export record tracks status, totalRows, processedRows, fileKey, filters, ids, selectedFields, progress, lastCursor (resume point), errorMessage, jobId.
Shared
| Operation | Key arguments |
|---|---|
importExportTypes | operation: IMPORT or EXPORT; returns registered types with their required permissions |
Running an import
Upload the file and get a fileKey
Imports read
fileKeyfrom the platform file storage (the same store/read-file?key=serves). The UI uploads the CSV first; the worker then streams it.processCSVStream(importUtils.ts) parses one row at a time throughcsv-parse(BOM-stripped, quotes relaxed), so memory stays O(1 row) even for large files.Preview and map columns
Call
importColumnPreviewwith thefileKey. It returns detected columns withconfidence/statusand the targetfields(required,type,options). Download the canonical header set anytime fromGET /import-export/download-template?entityType=core:contacts.customers(templates live intrpc/templates.ts).Start and watch
Call
importStartwithcolumnMapping(index→ targetkey). PollimportProgressforstatus,progress,rowsPerSecond,estimatedSecondsRemaining. Failures collect intoerrorRowsand a downloadableerrorFileUrl(workers/utils/errorFileHandler.ts).processCSVStreamaborts the job on stream errors, so a partial count means the file was truncated or unreadable rather than silently skipped.Export round-trip
Call
exportHeadersto pickselectedFields, thenexportStartwithfiltersor explicitids. Whenstatuscompletes, download from the generatedfileKey. Exports resume fromlastCursorafterexportRetry.
Job stays queued or every row errors
A job that never leaves the queue means the plugin's API is not running: workers start inside each service, not a separate process. Look for [ImportExport] Worker ready for queue <plugin>-import-processor in the service logs. If every row errors, the column keys do not match importFields; rerun importColumnPreview and fix columnMapping.
Permissions
importsManage and exportsManage (core module importExport) gate the job operations themselves; each entity type additionally requires its own action listed by importExportTypes (for example customersImportManage for core:contacts.customers).
Example
mutation StartCustomerImport {
importStart(
entityType: "core:contacts.customers"
fileKey: "imports/2025/customers.csv"
fileName: "customers.csv"
columnMapping: [
{ index: 0, header: "First Name", key: "firstName" }
{ index: 1, header: "Email", key: "primaryEmail" }
]
) { _id status }
}
query Watch($importId: String!) {
importProgress(importId: $importId) {
status
progress
processedRows
totalRows
errorRows
errorFileUrl
}
}