obsidian/wiki/payloadcms/plugin-import-export.md
2026-05-15 16:17:45 +01:00

8.4 KiB

title aliases tags sources created updated
Import/Export Plugin
payload-import-export
plugin-import-export
csv-export-payload
payloadcms
plugin
import
export
csv
json
etl
jobs-queue
raw/plugins__import-export.md
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:

  1. Admin UI — Download (streams directly) or Save (stores in exports collection)
  2. Local APIpayload.create({ collection: 'exports', data: { collectionSlug: 'pages', format: 'json' } })
  3. Jobs Queuepayload.jobs.queue({ task: 'createCollectionExport', input: ... })
  4. RESTPOST /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 APIpayload.create({ collection: 'imports', data: { collectionSlug, importMode }, file })
  3. Jobs Queuepayload.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:

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, 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:

{
  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.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

Sources