261 lines
8.4 KiB
Markdown
261 lines
8.4 KiB
Markdown
---
|
|
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_<blockSlug>_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
|