8.4 KiB
| title | aliases | tags | sources | created | updated | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Import/Export Plugin |
|
|
|
2026-05-15 | 2026-05-15 |
Import/Export Plugin
Adds CSV and JSON import/export to the Payload admin UI. Exports can be downloaded directly or saved as upload documents; imports support create/update/upsert modes.
Installation
pnpm add @payloadcms/plugin-import-export
Basic Setup
import { importExportPlugin } from '@payloadcms/plugin-import-export'
buildConfig({
plugins: [
importExportPlugin({
collections: [{ slug: 'users' }, { slug: 'pages' }],
}),
],
})
Jobs Queue — Required
By default all operations run through wiki/payloadcms/jobs-queue. Without a runner, imports/exports stay pending forever.
jobs: {
autoRun: [{ cron: '*/5 * * * *', queue: 'default' }],
},
For small datasets or testing, bypass the queue per-collection:
{ slug: 'pages', export: { disableJobsQueue: true }, import: { disableJobsQueue: true } }
Plugin-Level Options
| Property | Type | Description |
|---|---|---|
collections |
array | Which collections to enable. Defaults to all. |
exportLimit |
number | fn |
Global max docs per export (0 = unlimited). |
importLimit |
number | fn |
Global max docs per import (0 = unlimited). |
overrideExportCollection |
function | Modify the shared exports upload collection (e.g. add access control). |
overrideImportCollection |
function | Modify the shared imports upload collection. |
debug |
boolean | Enable debug logging. |
Per-Collection Config
collections: [
{
slug: 'pages',
export: {
format: 'csv', // force format
batchSize: 100, // docs per batch
limit: 1000, // override global exportLimit
disableDownload: true, // hide Download button
disableSave: false, // show Save button
hooks: { before, after },
},
import: {
defaultVersionStatus: 'draft',
batchSize: 100,
limit: 500,
hooks: { before, after },
},
},
{
slug: 'posts',
export: false, // disable export entirely
},
]
Exporting
Four methods:
- Admin UI — Download (streams directly) or Save (stores in
exportscollection) - Local API —
payload.create({ collection: 'exports', data: { collectionSlug: 'pages', format: 'json' } }) - Jobs Queue —
payload.jobs.queue({ task: 'createCollectionExport', input: ... }) - REST —
POST /api/exports/download
Selection modes in UI: all documents / current filters / checked selection.
Export parameters: format, limit, page, sort, depth (default 1), locale, drafts, fields, where, filename.
Importing
Four methods:
- Admin UI — Import drawer from collection list view
- Local API —
payload.create({ collection: 'imports', data: { collectionSlug, importMode }, file }) - Jobs Queue —
payload.jobs.queue({ task: 'createCollectionImport', input: ... }) - File storage — upload to
importscollection directly
Import modes:
| Mode | Behaviour |
|---|---|
create |
Create only — fails on duplicate IDs |
update |
Update only — requires id column, fails on missing IDs |
upsert |
Create or update — most flexible |
Match by non-ID field with matchField:
data: { collectionSlug: 'users', importMode: 'upsert', matchField: 'email' }
Import result fields: status, summary.total, summary.imported, summary.updated, summary.issues, summary.issueDetails.
Hooks
Hooks fire once per batch for both CSV and JSON.
export: {
hooks: {
// Modify data before write — return modified array
before: ({ data, format, batchNumber, totalBatches, originalData, req }) => {
return data.map(row => { const { passwordHash: _, ...safe } = row; return safe })
},
// After write — return value ignored, use for logging
after: ({ batchNumber, totalBatches, data, format, req }) => { ... },
},
},
import: {
hooks: {
before: ({ data }) => data.map(doc => ({ ...doc, email: doc.email?.toLowerCase() })),
// ImportAfterHook receives `result` instead of `data`
after: ({ result, req }) => { req.payload.logger.info({ imported: result.imported }) },
},
},
Execution order: field-level hooks → collection-level hooks.
Column Name Mapping (foreign systems)
// Export: rename Payload keys → foreign column names
before: ({ data }) => data.map(row => {
const map = { title: 'Post Title', excerpt: 'Summary' }
return Object.fromEntries(Object.entries(row).map(([k, v]) => [map[k] ?? k, v]))
}),
// Import: rename foreign columns → Payload field names
before: ({ data }) => data.map(doc => {
const map = { 'Post Title': 'title', 'Summary': 'excerpt' }
return Object.fromEntries(
Object.entries(doc).filter(([k]) => map[k]).map(([k, v]) => [map[k], v])
)
}),
Field-Level Options
Configure via custom['plugin-import-export'] on any field:
{
name: 'internalField',
type: 'text',
custom: {
'plugin-import-export': {
disabled: true, // exclude from all import/export
hooks: {
beforeExport: ({ value, siblingData, format }) => { ... },
beforeImport: ({ value, data, format }) => { ... },
},
},
},
}
beforeExport— return value to replace field,undefinedfor default, or mutatesiblingDatato add extra columnsbeforeImport— return transformed value,undefinedto skip,nullto explicitly set null- Virtual fields: exported (computed values), but skipped on import
CSV Format Details
- Nested fields:
group_value,array_0_field - Blocks:
blocks_0_<blockSlug>_blockType - Localized:
title_en,title_es(multi-locale withlocale: 'all') - Relationship has-one:
fieldName(monomorphic),fieldName_relationTo+fieldName_id(polymorphic) - Relationship has-many:
fieldName_0,fieldName_1(monomorphic) - Auto-coercion:
"true"/"false"→ boolean,"null"→ null, numeric strings → number
Separate Export Collections per Data Type
Override the slug to create isolated upload collections with different access control:
{
slug: 'users',
export: {
overrideCollection: ({ collection }) => ({
...collection,
slug: 'user-exports',
access: { read: ({ req }) => req.user?.role === 'superadmin' },
}),
},
}
Collections sharing the default exports/imports slug are unaffected.
Admin UI Visibility
exports/imports collections are hidden from nav by default. To show them:
overrideExportCollection: ({ collection }) => ({
...collection,
admin: { ...collection.admin, group: 'Data Management' },
}),
Access Control Warning
Users with read access to the exports upload collection can download all exported data, bypassing per-document access control on the source collection. Always add access control via overrideExportCollection.
Key Takeaways
- Jobs Queue required by default — configure
jobs.autoRunor setdisableJobsQueue: truefor small/test datasets - Four export methods: admin UI download, admin UI save, Local API, Jobs Queue
- Three import modes:
create/update/upsert; usematchFieldto match by non-ID field - Hooks fire per batch, not per document — return modified array in
before, useafterfor logging - Column mapping via hooks:
export.hooks.beforerenames out,import.hooks.beforerenames in; field-levelbeforeExportis alternative for shared fields - Access control gap:
exportsupload collection bypasses source-collection ACL — add guards viaoverrideExportCollection - Virtual fields: included in exports (computed), skipped on import
- CSV auto-coercion:
"null"→ null,"true"→ boolean, numeric strings → number; usebeforeImporthook to preserve literal strings
Related
- wiki/payloadcms/jobs-queue
- wiki/payloadcms/jobs-queue-queues
- wiki/payloadcms/upload
- wiki/payloadcms/access-control
- wiki/payloadcms/hooks-collections
- wiki/payloadcms/plugins
Sources
raw/plugins__import-export.md- https://payloadcms.com/docs/plugins/import-export