--- title: "Import/Export Plugin" aliases: [payload-import-export, plugin-import-export, csv-export-payload] tags: [payloadcms, plugin, import, export, csv, json, etl, jobs-queue] sources: [raw/plugins__import-export.md] created: 2026-05-15 updated: 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 ```bash pnpm add @payloadcms/plugin-import-export ``` ## Basic Setup ```ts 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|Payload's Jobs Queue]]. Without a runner, imports/exports stay `pending` forever. ```ts jobs: { autoRun: [{ cron: '*/5 * * * *', queue: 'default' }], }, ``` For small datasets or testing, bypass the queue per-collection: ```ts { 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 ```ts 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: 1. **Admin UI** — Download (streams directly) or Save (stores in `exports` collection) 2. **Local API** — `payload.create({ collection: 'exports', data: { collectionSlug: 'pages', format: 'json' } })` 3. **Jobs Queue** — `payload.jobs.queue({ task: 'createCollectionExport', input: ... })` 4. **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: 1. **Admin UI** — Import drawer from collection list view 2. **Local API** — `payload.create({ collection: 'imports', data: { collectionSlug, importMode }, file })` 3. **Jobs Queue** — `payload.jobs.queue({ task: 'createCollectionImport', input: ... })` 4. **File storage** — upload to `imports` collection 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`: ```ts 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. ```ts 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) ```ts // 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: ```ts { 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, `undefined` for default, or mutate `siblingData` to add extra columns - `beforeImport` — return transformed value, `undefined` to skip, `null` to 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__blockType` - Localized: `title_en`, `title_es` (multi-locale with `locale: '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: ```ts { 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: ```ts 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.autoRun` or set `disableJobsQueue: true` for small/test datasets - **Four export methods:** admin UI download, admin UI save, Local API, Jobs Queue - **Three import modes:** `create` / `update` / `upsert`; use `matchField` to match by non-ID field - **Hooks fire per batch**, not per document — return modified array in `before`, use `after` for logging - **Column mapping via hooks:** `export.hooks.before` renames out, `import.hooks.before` renames in; field-level `beforeExport` is alternative for shared fields - **Access control gap:** `exports` upload collection bypasses source-collection ACL — add guards via `overrideExportCollection` - **Virtual fields:** included in exports (computed), skipped on import - **CSV auto-coercion:** `"null"` → null, `"true"` → boolean, numeric strings → number; use `beforeImport` hook to preserve literal strings ## Related - [[wiki/payloadcms/jobs-queue|Jobs Queue — Overview]] - [[wiki/payloadcms/jobs-queue-queues|Jobs Queue — Queues & Execution]] - [[wiki/payloadcms/upload|Upload & Media]] - [[wiki/payloadcms/access-control|Access Control]] - [[wiki/payloadcms/hooks-collections|Collection Hooks]] - [[wiki/payloadcms/plugins|Official Plugins]] ## Sources - `raw/plugins__import-export.md` - https://payloadcms.com/docs/plugins/import-export