---
title: "How to Export Data from Plone: Methods, API Limits & Portability"
slug: how-to-export-data-from-plone-methods-api-limits-portability
date: 2026-08-20
author: Rishabh
categories: [Migration Guide, Knowledge Base]
excerpt: "Complete guide to extracting data from Plone: REST API batching, collective.exportimport, plone.exportimport, .zexp, Data.fs — with version-specific recommendations and failure modes."
tldr: "Plone has no single export button. Use plone.exportimport on 6.1+, collective.exportimport for Plone 4–6 cross-version migrations, or plone.restapi for custom scripted extraction. Each method has coverage gaps."
canonical: https://clonepartner.com/blog/how-to-export-data-from-plone-methods-api-limits-portability/
---

# How to Export Data from Plone: Methods, API Limits & Portability


**Plone has no single "Export Everything" button.** Your export method depends on your Plone version, the content framework in use (Archetypes vs. Dexterity), and whether you're migrating to another Plone instance or leaving the ecosystem entirely.

Because Plone is built on Python and stores data in the **Zope Object Database (ZODB)** rather than a relational SQL database, you cannot run `mysqldump` or query business tables with SQL. Even when Plone uses PostgreSQL through RelStorage, the data is stored as pickled Python objects. The [RelStorage FAQ](https://relstorage.readthedocs.io/en/latest/faq.html) confirms you cannot perform SQL queries against this data in the way most teams expect. To read it, you must wake up the Python objects within the Plone application context.

This architectural reality dictates every export method available to you. A **migration-ready extract** — content, blobs, users, permissions, relations, translations, redirects — typically requires combining more than one tool. A **backup** (copying `Data.fs` and blob storage) gives you a restorable copy of the same site, not an extract another platform can ingest.

> [!NOTE]
> **TL;DR:** Use `plone.exportimport` for full-site JSON exports on Plone 6.1+. Use `collective.exportimport` for cross-version migrations from Plone 4–6. Use `plone.restapi` for programmatic, paginated extraction or continuous sync. Raw database backups are restore artifacts, not migration-ready exports. Each method has coverage gaps.

## Plone Export Methods Compared

| Method | Content | Users / Groups | Relations | Translations | Version History | Binary Files | Best For |
|--------|---------|---------------|-----------|-------------|----------------|-------------|----------|
| **plone.restapi (`@search`)** | ✅ (paginated JSON) | Via `@users` / `@groups` | Separate calls | ❌ | ❌ | Separate download | Custom scripted extraction |
| **plone.exportimport (`@export`)** | ✅ | ✅ | ✅ | ✅ | Opt-in (`--include-revisions`) | ✅ (in ZIP) | Plone 6.1+ site-to-site |
| **collective.exportimport** | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ (base64, blob paths, or download URLs) | Cross-version migration (Plone 4→6) |
| **collective.jsonify** | ✅ (one JSON per object) | Supplementary scripts | ❌ | ❌ | ❌ | ✅ (base64) | Legacy Plone 2.1–4.x export |
| **Zope .zexp (ZMI)** | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | Identical-version cloning only |
| **Data.fs direct copy** | ✅ (entire DB) | ✅ | ✅ | ✅ | ✅ | Blob storage separate | Full backup / same-version restore |
| **Transmogrifier pipelines** | ✅ | Configurable | Configurable | Configurable | ❌ | ✅ | Complex ETL pipelines |

## plone.restapi: The Primary API Export Path

`plone.restapi` is the standard programmatic interface for extracting content from Plone. It is included in Plone 6 when you install the `Plone` package. Version 10 requires Python 3 and works with Plone 6.2, 6.1, 6.0, and 5.2. On Plone 5.x, it is available as an installable add-on.

The API uses content negotiation — set the `Accept` header to `application/json` to get JSON responses. Authentication supports both HTTP Basic and JWT tokens.

### Authentication

`plone.restapi` includes a Plone PAS plugin for JWT authentication, installed automatically with the product. To obtain a token:

```bash
curl -X POST http://your-plone-site/Plone/@login \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data '{"login": "admin", "password": "secret"}'
```

The response includes a `token` field you use in subsequent requests via the `Authorization: Bearer <token>` header.

### Batching and Pagination

Collection-like resources are batched if the result set exceeds the batch size. The **default batch size is 25**.

You control pagination with two query parameters:

- **`b_start`** — Index of the first item in the batch (default: 0)
- **`b_size`** — Number of items per batch (default: 25)

The response includes a `batching` object with hypermedia links (`first`, `last`, `prev`, `next`) that your client can follow. The `items_total` field gives you the total result count.

Unlike SaaS platforms with [strict vendor-imposed rate limits](https://clonepartner.com/blog/blog/how-to-export-data-from-helpjuice-methods-api-limits-portability/), Plone is self-hosted. Your throughput limits are dictated by your server's RAM and WSGI server configuration (Waitress, Gunicorn). But you must still paginate — requesting too many heavy objects at once causes memory spikes and timeouts.

> [!WARNING]
> There is no official `b_size=-1` parameter to disable batching. Setting `b_size` to a very large number works but can cause memory issues on large sites. The safest approach is to iterate through pages using the `next` link in the batching response.

### Extracting Content via `@search`

The `@search` endpoint maps to Plone's `portal_catalog`. You can query by content type, path, date ranges, and any indexed field:

```bash
curl "http://your-plone-site/Plone/@search?portal_type=Document&b_size=100" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer <token>"
```

By default, `@search` returns **catalog brain summaries** — not full content. Adding `?fullobjects=true` returns complete JSON representations, but Plone must wake each object from the ZODB, which is significantly slower. On a site with 100,000 items, `fullobjects=true` means 100,000 individual ZODB object loads; budget for this to take several hours on modest hardware.

Two details catch people regularly. First, `@search` on a container includes the container itself unless you constrain depth with `path.depth`. Second, `SearchableText` does **not** auto-append a trailing wildcard the way Plone's LiveSearch does — add the `*` yourself if you want prefix matching.

### REST API Edge Cases

- **Summaries ≠ full objects.** `metadata_fields=_all` returns less than `fullobjects=true`, and some fields requested as metadata can fail JSON conversion.
- **`fullobjects=true` has a real cost.** For a site with 100,000 items, fetching each object individually means 100,000 ZODB object loads.
- **Folder GETs include children.** Folderish content includes child `items` automatically; use `include_items=false` to avoid unexpected payload expansion.
- **Files are always a second hop.** Serialized file/image fields give you filename, MIME type, size, and a `download` URL. You need a separate GET for the bytes.
- **User export supports CSV.** The `@users` endpoint can return CSV as of `plone.restapi` 10.0.0rc1.
- **Groups are separate from users.** The `@groups` endpoint defaults to 25 groups at a time unless you raise the `limit` parameter.
- **Registry export is admin-only.** Reading or writing `@registry` records requires `cmf.ManagePortal`.

### What the REST API Does Not Export

The REST API gives you content objects, users, groups, workflow states, and vocabularies. It does **not** provide a single-call export for:

- Relations between objects (requires separate per-object inspection)
- Content ordering (position in parent)
- Local roles / sharing settings (per-object `@sharing` endpoint, not bulk-exportable)
- Translation linkage (depends on `plone.app.multilingual`; no dedicated bulk export endpoint)
- Portal configuration (registry, control panel settings)
- Portlet assignments
- URL redirects

For migration-grade extraction, the REST API is a building block — not a complete solution.

### Step-by-Step: REST API Extraction Script

For teams writing custom extraction scripts, here's the practical workflow:

**1. Authenticate and get a token:**

```bash
TOKEN=$(curl -s -X POST http://localhost:8080/Plone/@login \
  -H "Content-Type: application/json" \
  -d '{"login":"admin","password":"secret"}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
```

**2. Enumerate content types:**

```bash
curl -s "http://localhost:8080/Plone/@types" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer $TOKEN"
```

**3. Paginate through all content:**

```python
import requests

base = "http://localhost:8080/Plone"
headers = {"Accept": "application/json", "Authorization": f"Bearer {TOKEN}"}

def export_all_content(portal_type):
    url = f"{base}/@search?portal_type={portal_type}&fullobjects=true&b_size=50"
    all_items = []
    while url:
        resp = requests.get(url, headers=headers).json()
        all_items.extend(resp.get("items", []))
        url = resp.get("batching", {}).get("next")
    return all_items
```

**4. Download binary files separately:**

```python
for item in items:
    if "file" in item and item["file"]:
        file_url = item["file"]["download"]
        binary = requests.get(file_url, headers=headers).content
        # Save to disk
```

**5. Export supplementary data:**

```bash
curl -s "http://localhost:8080/Plone/@users" \
  -H "Accept: application/json" -H "Authorization: Bearer $TOKEN"

curl -s "http://localhost:8080/Plone/@groups" \
  -H "Accept: application/json" -H "Authorization: Bearer $TOKEN"
```

## plone.exportimport: The Plone 6.1+ Official Export

`plone.exportimport` provides support to export and import all kinds of data from and to Plone sites using an intermediate JSON format. Most features use `plone.restapi` to serialize and deserialize data under the hood. This is the closest thing Plone has to a full site export — covering content, principals, relations, translations, discussions, redirects, and (optionally) version history. ([Plone docs](https://6.docs.plone.org/admin-guide/export-import.html))

### Two Export Interfaces

**Command-line** (server-side access required):

```bash
plone-exporter instance/etc/zope.conf Plone /tmp/plone_data/
plone-exporter --include-revisions instance/etc/zope.conf Plone /tmp/plone_data/
```

By default, revision history (older versions of each content item, stored in `CMFEditions`) is **not** exported. Add `--include-revisions` if you need it. On large sites with active editorial workflows, version history stored in `CMFEditions` can make your `Data.fs` 10–50× larger than the current content alone — exporting revisions substantially increases both export time and output size.

**REST API endpoint:**

```bash
curl -X POST \
  -H "Authorization: Bearer <token>" \
  https://plone.example.com/plone/++api++/@export \
  -o export.zip
```

The `@export` endpoint requires the `plone.exportimport.export` permission, granted to Site Administrators by default. The response is a ZIP file containing all exported data. On sites with 50,000+ objects, this endpoint runs synchronously and is prone to browser and HTTP proxy timeouts — use the command-line `plone-exporter` for large sites.

### Export Structure

The exported layout is filesystem-friendly and easy to inspect. Each content item gets its own subdirectory named by UID, containing serialized data and blob files:

```text
content/
  __metadata__.json
  plone_site_root/
  <UID>/
    data.json
    file/<filename>
principals.json
relations.json
translations.json
discussions.json
redirects.json
```

This structure makes it straightforward to diff, inspect, and transform compared to a raw database snapshot.

> [!WARNING]
> `plone.exportimport` targets current Plone and Python versions only. It does **not** support Plone 4/5 or handle Archetypes-to-Dexterity conversion. If you're on an older version, you need `collective.exportimport` instead.

## collective.exportimport: The Cross-Version Migration Workhorse

This is the package the Plone community uses for major version migrations. It allows you to migrate from Plone 4 to 6, from Python 2 to 3, and from Archetypes to Dexterity in one step. Plone's own [upgrade documentation](https://6.docs.plone.org/backend/upgrading/intro.html) recommends it for large and complex migrations.

### What It Exports

Each data type has its own browser view:

- `/@@export_content` — Content items (select by type or export all)
- `/@@export_members` — Users, groups, and role assignments
- `/@@export_relations` — Object relations
- `/@@export_localroles` — Per-object sharing/permissions
- `/@@export_translations` — Translation links
- `/@@export_ordering` — Item positions within folders
- `/@@export_discussion` — Comments/discussions

If you select "Save to file on server," the export saves JSON files to the `<var>` directory of your Plone instance. The import view looks for files under `/var/instance/import`.

### Blob Handling Strategies

[`collective.exportimport`](https://github.com/collective/collective.exportimport) gives you three strategies for binary files:

1. **Base64 encoding:** Embeds the file directly in the JSON. Avoid for large sites — base64 inflates binary size by approximately 33% and loading all blobs into memory simultaneously causes severe memory bloat.
2. **Blob paths:** Exports the absolute filesystem path to the blob. Plone's migration training recommends this option when you can access the old blob store directly. This is the most memory-efficient option.
3. **Download URLs:** References the file via URL for later retrieval. Useful when the source site remains accessible during import.

### Key Constraints

- **Export format is JSON** — one file per content type (e.g., `Document.json`, `Image.json`)
- **No version history** — only current versions are exported
- **Modified dates get reset during import.** A separate `@@reset_dates` view resets modification dates to the values in the exported JSON, since import-time operations update the modified date. The original is stored as `obj.modification_date_migrated` on each new object.
- **Requires server-side access** — you must install the package and access the Plone instance directly
- **Performance is significant.** Per the project README, importing 5,000 Documents takes approximately 25 minutes with versioning enabled and approximately 7 minutes without. For a 50,000-document site, that translates to roughly 4–5 hours of import time with versioning. Rehearse with production-like data volumes before scheduling a maintenance window.

## The Volto Block Problem

If you're exporting from **Plone 6 with Volto** (the modern React frontend), you'll hit a data mapping challenge that no export tool solves on its own.

In Classic Plone, rich text is stored as standard HTML in a `RichTextValue` field. Most target systems — WordPress, Contentful, SharePoint — can ingest HTML directly.

In Volto, content is not stored as HTML. It lives in the `blocks` and `blocks_layout` fields as **Slate JSON**. A simple bold paragraph looks like this in the export:

```json
{
  "@type": "slate",
  "plaintext": "This is a paragraph.",
  "value": [
    {
      "children": [
        { "text": "This is a " },
        { "text": "paragraph.", "strong": true }
      ],
      "type": "p"
    }
  ]
}
```

**No other CMS natively understands Plone's Volto block schema.** Much like dealing with [proprietary HubL code during a HubSpot CMS export](https://clonepartner.com/blog/blog/how-to-export-data-from-hubspot-cms-hub-methods-apis-limits/), you will need middleware to parse the `blocks` JSON and convert it into either standard HTML or the specific block format of your target system. The mapping is non-trivial:

| Volto Block Type | WordPress Equivalent | Contentful Equivalent | Sanity Equivalent |
|-----------------|---------------------|----------------------|------------------|
| `slate` (rich text) | `core/paragraph` block | Rich Text field | Portable Text array |
| `image` | `core/image` block | Asset reference | Image object |
| `video` | `core/embed` block | No native equivalent | External URL field |
| `listing` | Dynamic query block (plugin) | No native equivalent | Custom query type |
| `toc` (table of contents) | `generateblocks/table-of-contents` | No native equivalent | Custom component |
| Custom add-on blocks | Requires custom plugin | Requires custom field type | Requires custom schema |

The `blocks_layout` field stores the display order as an array of UUIDs referencing keys in the `blocks` object. Any conversion tool must respect this ordering and handle blocks it doesn't recognize gracefully (typically by falling back to the `plaintext` property if present, or skipping the block).

> [!TIP]
> When migrating to a non-Plone target, exporting via `plone.restapi` with `fullobjects=true` gives you the most portable JSON serialization, including resolved field values rather than internal references. For Volto sites, this is the only way to get the full `blocks` payload — the catalog brain summaries returned by `@search` without `fullobjects=true` omit block content entirely.

## collective.jsonify: Legacy Plone Export (2.1+)

For very old Plone installations where neither `plone.restapi` nor `collective.exportimport` can be installed, `collective.jsonify` is the fallback. Its only dependency is `simplejson`, and it can be installed on any Plone version as far back as **Plone 2.1**.

The export produces one JSON file per content object, executed via External Methods registered through the Zope Management Interface (ZMI). The output format is `collective.transmogrifier`-compatible, meaning each JSON file maps directly to a pipeline item that transmogrifier can process — it is not directly importable by WordPress, Drupal, or other non-Plone platforms without an additional transformation layer.

To migrate Plone 1, 2, or 3 to Plone 6, the recommended pipeline is `collective.jsonify` for export and `collective.exportimport` for import.

**Limitations:**
- No export of relations, translations, or local roles without supplementary scripts
- No web UI or progress indicator — export progress must be monitored via Zope logs
- Base64-encoded binaries (memory-intensive for large file collections)
- No version history export
- Output is `collective.transmogrifier`-compatible JSON, not directly importable by non-Plone systems

## Transmogrifier Pipelines: ETL for Plone

Transmogrifier is a pipeline-based ETL tool for Plone content migration. Pipelines are defined in `.ini`-like configuration files. Each pipeline consists of ordered **blueprints** — reusable processing components that each receive a content item dict, optionally transform it, and pass it to the next blueprint in the chain.

A minimal transmogrifier pipeline for exporting content to JSON looks like:

```ini
[transmogrifier]
pipeline =
    source
    exporter
    saver

[source]
blueprint = collective.transmogrifier.sections.csvsource
filename = items.csv

[exporter]
blueprint = collective.transmogrifier.sections.logger
name = exporter

[saver]
blueprint = collective.transmogrifier.sections.jsonwriter
directory = /tmp/export
```

Blueprints can read from the ZODB directly, apply field transformations, filter by content type, rename attributes, or write to arbitrary output formats. This flexibility makes Transmogrifier suited for migrations that require content transformation during export — renaming fields, splitting content types, merging folders. The trade-off is a steep learning curve and the requirement for deep Plone engineering knowledge. For straightforward exports, `collective.exportimport` has largely replaced it.

## Exporting Users, Groups, and Permissions

Plone handles authentication and authorization via the **Pluggable Auth Service (PAS)**. Exporting content does **not** automatically export users or their permissions — these are separate data tracks.

- With **`collective.exportimport`**, run `@@export_members` for users and groups, and `@@export_localroles` for per-object permissions.
- With **`plone.restapi`**, users come from `@users`, groups from `@groups`, and local roles from the per-object `@sharing` endpoint (not bulk-exportable in a single call).
- With **`plone.exportimport`**, users and groups are included in the `principals.json` output automatically.

**Local roles** deserve particular attention. Plone allows permissions at the folder or document level — granting a specific user "Editor" rights on a single subfolder, for example. The permission model uses the following built-in roles: Reader, Editor, Contributor, Reviewer, Site Administrator. Extracting the granular per-object permission matrix requires walking the full object tree and calling `@sharing` on each object, or using `@@export_localroles` from `collective.exportimport`.

If you're migrating to a system with a simpler permission model (Webflow, basic SharePoint sites, most headless CMS platforms), you'll need to flatten this architecture into something the target can represent — typically by mapping all local role assignments to a single flat permission tier.

## Exporting to WordPress: Field Mapping Reference

"Plone to WordPress" is one of the most common migration queries. WordPress ingests content via the WordPress REST API or the WXR (WordPress eXtended RSS) XML format. Here is how Plone fields map:

| Plone Field | WordPress Equivalent | Notes |
|------------|---------------------|-------|
| `title` | `post_title` | Direct mapping |
| `description` | `post_excerpt` | Direct mapping |
| `text` (Classic HTML) | `post_content` | Direct mapping; HTML passthrough |
| `blocks` (Volto JSON) | `post_content` as Gutenberg blocks | Requires conversion; no standard tool exists |
| `effective` (publication date) | `post_date` | ISO 8601 format; convert timezone |
| `subjects` (tags) | `tags` | Create terms first, then assign by ID |
| `creators` | `post_author` | Must pre-create WP users; map by username |
| `review_state` | `post_status` | Map `published`→`publish`, `private`→`draft` |
| `image` (lead image) | `_thumbnail_id` (featured image) | Upload via media endpoint, then set meta |
| UID | Custom meta field (`_plone_uid`) | Store for redirect resolution |
| Original path | Custom meta field (`_plone_path`) | Required for redirect rule generation |

There is no off-the-shelf Plone-to-WordPress migration tool. The standard approach is: export via `plone.restapi` or `collective.exportimport`, write a Python or Node.js script to transform the JSON, and POST to the WordPress REST API or generate WXR XML for import.

## Choosing the Right Export Method by Plone Version

| Plone Version | Best Export Tool | Notes |
|---------------|-----------------|-------|
| **2.1–3.x** | `collective.jsonify` | Only option; pair with `collective.exportimport` for import on target |
| **4.x (Archetypes)** | `collective.exportimport` | Handles AT→Dexterity conversion; install via buildout |
| **5.0–5.2** | `collective.exportimport` + `plone.restapi` | REST API available as add-on on 5.x, core on 5.2 |
| **6.0** | `collective.exportimport` or `plone.exportimport` | Volto frontend; REST API is core |
| **6.1+** | `plone.exportimport` | Ships with Plone; `@export` endpoint available |

### Decision by Scenario

| Scenario | Recommended Method | Why |
|----------|-------------------|-----|
| Plone 6.1 → Plone 6.1 | `plone.exportimport` | Official, complete, single ZIP |
| Plone 4 → Plone 6 | `collective.exportimport` | Handles AT→Dexterity, Python 2→3 |
| Plone 2.x/3.x → anything | `collective.jsonify` + `collective.exportimport` | Only viable path for ancient versions |
| Plone → WordPress | `plone.restapi` scripted export + custom transform | Most portable JSON; requires field mapping script |
| Plone → Contentful/Sanity | `plone.restapi` scripted export + custom transform | Export schema must map to Contentful/Sanity content model |
| Plone → Drupal | `collective.exportimport` JSON + Drupal Migrate API | Drupal's migrate framework can consume structured JSON |
| Full backup / disaster recovery | `Data.fs` + blob storage copy | Complete, but version-locked |
| Complex content transformation | Transmogrifier pipeline | Maximum flexibility, highest effort |

## ZEXP and Data.fs: Backup Methods, Not Export Methods

### ZEXP

Zope's native `.zexp` format is available through the Zope Management Interface (ZMI). Navigate to the object, click "Import/Export," and download the file.

> [!CAUTION]
> ZEXP files are Python pickle data. Both source and target Zope instances must be identical — same Zope version, same Products installed, same Python version. Even minor version differences can cause unpickling failures that are difficult to diagnose. Do not use `.zexp` for cross-version migrations or export to non-Plone systems.

### Data.fs Direct Copy

Every Plone site stores its content in the ZODB, typically as `Data.fs` in `var/filestorage/`, with blob storage in `var/blobstorage/`. These are maintained synchronously; if they drift out of sync, you encounter `POSKeyError` errors when content is read.

This is a **backup** method, not an export method. The files are only usable by an identical or closely compatible Plone installation.

Data.fs copy is useful for: moving between identical servers, performing in-place upgrades within the same major version line, or creating a staging copy to run `collective.exportimport` against before the actual migration window.

> [!WARNING]
> **Do not hand a `Data.fs` or RelStorage snapshot to the target-system team and call it the export.** Use backup tooling when the question is "How do we restore this Plone site?" Use export tooling when the question is "How do we transform this data for another system?" Those are different jobs, and mixing them up creates bad migration plans.

## Common Failure Modes

Here are the failure patterns that appear most frequently in Plone export and migration projects:

1. **Blob storage not included in backup.** `Data.fs` doesn't contain large files if blob storage is enabled (standard since Plone 4). You must copy `var/blobstorage/` separately and keep the two in sync. A `Data.fs` without its matching blob store produces `POSKeyError` on any content with file attachments.

2. **Archetypes content on Plone 6.** If you skipped the AT→Dexterity migration, your content types may not serialize correctly via `plone.restapi`. Run the AT→Dexterity migration step first using `plone.app.contenttypes`, or use `collective.exportimport` which handles the conversion inline.

3. **Large sites timing out.** The `@export` endpoint and `@@export_content` views run synchronously. On sites with 50,000+ objects, browser and reverse-proxy timeouts are common. Use the command-line `plone-exporter` or the "Save to file on server" option in `collective.exportimport`.

4. **Revision history bloat.** Plone stores content versions in `CMFEditions`. These are not exported by default and can make `Data.fs` files 10–50× larger than the actual current content. If you export with `--include-revisions`, budget for substantially longer export and import times.

5. **Missing add-on content types.** If your site uses add-on types (`PloneFormGen`, `eea.facetednavigation`, `collective.dexteritytextindexer`-dependent types), the export tools may not serialize their custom fields correctly without explicit serializer adapters registered for those types.

6. **Encoding issues on Python 2.** Plone 4 sites running Python 2.7 can produce JSON with mixed byte strings and unicode. `collective.exportimport` handles this internally, but custom scripts that concatenate or process the raw output may break with `UnicodeDecodeError`.

7. **Form data schema drift.** KU Leuven's Plone 6 migration found that saved form data failed when stored rows no longer matched the current form structure. They kept the old Plone 4 site available for at least one year so users could view and download legacy data. ([KU Leuven migration notes](https://admin.kuleuven.be/icts/english/services/wms/plone6/migration-saved-data-forms))

8. **Modified dates silently change.** Import-time operations update modified dates. Run `@@reset_dates` after import if date integrity matters for audit trails or publication history.

9. **Volto blocks with no `plaintext` fallback.** Some custom Volto block types store no `plaintext` representation. If your conversion script relies on `plaintext` as a fallback for unrecognized block types, those blocks will silently produce empty content in the target. Audit custom block types before export.

10. **`portal_catalog` out of sync.** On old or poorly maintained Plone sites, the catalog may not reflect actual content (objects present in ZODB but not indexed, or catalog entries pointing to deleted objects). Run a full catalog rebuild before export to avoid gaps between `@search` results and actual content.

## Pre-Migration Export Checklist

Because Plone splits content, blobs, roles, redirects, and configuration across different tracks, your runbook should do the same.

1. **Inventory by object class, not just page count.** Count custom types, files/images, forms, multilingual branches, comments, and redirects. Query `portal_catalog` by `portal_type` to get per-type counts.
2. **Choose the export path by Plone version.** See the version table above.
3. **Decide blob strategy early.** REST download, per-item files on disk, base64 in JSON, or legacy blob-path access all have different performance and validation costs. Base64 is generally unsuitable for sites with more than a few hundred MB of binary content.
4. **Export secondary objects deliberately.** Do not assume users, groups, local roles, aliases, translations, relations, or version history come along "for free." Each requires an explicit export step.
5. **Preserve stable identifiers.** Keep `UID`, original path, and published URL history wherever the target can store them. These are essential for generating redirect rules.
6. **Rebuild the catalog before export.** Ensures `@search` results match actual content. Run through ZMI → `portal_catalog` → "Advanced" → "Clear and Rebuild."
7. **Validate with counts and spot checks.** Compare object counts by type, file counts, revision expectations, and redirect behavior between source and target.
8. **Test Volto block conversion separately.** If the source is a Volto site, extract a representative sample of each custom block type and verify your conversion produces correct output before running full export.
9. **Rehearse with production-like data.** Run at least one full dry run to measure timing, fix edge cases, and document rollback steps. Keep the old site available long enough for delta checks.

## Getting the Data Out Is the Easy Part

Plone migrations are among the more technically demanding data extraction jobs. The combination of ZODB storage, version-dependent serialization, the Archetypes-to-Dexterity transition, and Volto's block format means there is no "one-click" path — especially when the target isn't another Plone instance.

The hard part is rarely getting bytes out of Plone. As is true when [extracting data from other complex enterprise platforms](https://clonepartner.com/blog/blog/how-to-export-data-from-fintechos-methods-limits-formats-2026/), the hard part is rebuilding structure in the target: binaries, roles, redirects, translations, and content relationships — without losing trust in the data. The Volto block schema, the local roles matrix, and add-on content type fields are the three areas most likely to require custom engineering regardless of which export tool you choose.

> Planning a Plone migration? Whether you're moving off a Plone 4 site or extracting content for a platform switch, our team has handled complex ZODB-backed exports. Book a 30-minute call and we'll map out your extraction path.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I export Plone data using a SQL database dump?

No. Even if Plone uses PostgreSQL via RelStorage, the data is stored as serialized Python pickles. The RelStorage FAQ confirms you cannot perform useful SQL queries against this data. You must extract data through Plone's Python environment using tools like collective.exportimport, plone.exportimport, or plone.restapi.

### How do I export all content from a Plone site?

On Plone 6.1+, use the plone.exportimport @export endpoint or plone-exporter CLI to get a ZIP of all content, users, relations, and translations. On Plone 4–5, install collective.exportimport and use the @@export_content browser view. For Plone 2.x–3.x, use collective.jsonify (one JSON file per object) and pair it with collective.exportimport for import on the target.

### Does the Plone REST API have rate limits?

Plone is self-hosted, so there are no vendor-imposed rate limits. However, plone.restapi paginates results with a default batch size of 25 items (b_size parameter). Performance is bound by ZODB I/O — using fullobjects=true on large result sets can be very slow as each object must be woken from the database.

### What is the difference between collective.exportimport and plone.exportimport?

collective.exportimport supports Plone 4–6, handles Archetypes-to-Dexterity conversion, and works with Python 2 and 3. plone.exportimport ships with Plone 6.1+, focuses only on latest Plone/Python, and provides a REST API @export endpoint. Use collective.exportimport for cross-version migrations and plone.exportimport for Plone 6.1+ exports.

### What format does Plone 6 Volto export rich text in?

If you are using Plone 6 with the Volto frontend, rich text is stored as Slate JSON blocks in the blocks and blocks_layout fields, not standard HTML. No other CMS natively reads this format — you will need to write middleware to convert the block JSON into HTML or your target system's block schema.
