---
title: "How to Export Data from October CMS: Methods, Limits & Portability"
slug: how-to-export-data-from-october-cms-methods-limits-portability
date: 2026-08-20
author: Abdul
categories: [Migration Guide, General]
excerpt: "October CMS has no single export button. Learn every extraction method — DB dumps, backend exports, custom APIs, Tailor content, and file attachments — with real constraints and edge cases."
tldr: "Export October CMS by combining mysqldump for the database, filesystem copies for themes/media/uploads, and custom Eloquent queries to resolve polymorphic file attachments."
canonical: https://clonepartner.com/blog/how-to-export-data-from-october-cms-methods-limits-portability/
---

# How to Export Data from October CMS: Methods, Limits & Portability


# How to Export Data from October CMS: Methods, Limits & Portability

> [!NOTE]
> **TL;DR:** October CMS has no single "export everything" button. A complete export combines a database dump (MySQL, PostgreSQL, or SQLite), filesystem copies of themes/media/uploads, and custom scripting to resolve polymorphic file attachments and Tailor content. There are no vendor-imposed API limits — your constraints are server-level PHP and web server configurations.

October CMS is a self-hosted Laravel application, which means you have full server access. Unlike SaaS platforms that gate your data behind [paginated APIs and rate limits](https://clonepartner.com/blog/blog/how-to-export-data-from-hubspot-cms-hub-methods-apis-limits/), you control the database, the filesystem, and the runtime. That is an advantage — but it does not mean exporting is simple.

The data model is fragmented by design. Content is split across relational database tables, the `system_files` polymorphic attachment table, flat theme files, the media library filesystem, and plugin-specific storage where each plugin defines its own schema. Getting a complete, migration-ready export means combining multiple extraction methods and understanding exactly where each piece of data lives.

This guide covers every viable extraction method, the exact constraints you will hit, and the failure modes that catch teams mid-migration.

**Verified against:** October CMS v4.x (up to v4.3) running on Laravel 12. Older versions (v2.x, v3.x) share similar export mechanics but differ in Tailor availability and some artisan commands.

## What Data Lives Where in October CMS

Before writing any scripts, map where your data actually resides. October CMS splits data between the database and the filesystem, and different subsystems use different storage strategies.

| Data Layer | Storage Location | Export Method | What People Miss |
|---|---|---|---|
| **CMS Pages, Layouts, Partials** | Flat `.htm` files in `themes/your-theme/` | Filesystem copy (rsync/tar) | If database-driven themes are enabled, the live version may be in the DB, not on disk |
| **Tailor Content (v3+)** | Auto-generated DB tables from YAML blueprints | `mysqldump` / Eloquent queries / custom API | Table names are generated from blueprint UUIDs — not human-readable. Removed fields can linger until `tailor:prune` |
| **Plugin Content** (Blog, FAQ, etc.) | Plugin-specific DB tables (e.g., `rainlab_blog_posts`) | `mysqldump` / ExportModel CSV / custom API | Each plugin defines its own schema; no universal export format |
| **File Attachments** | `system_files` table + `storage/app/uploads/` | DB query + filesystem copy | Polymorphic relations — must preserve `attachment_type`, `attachment_id`, and `field` mappings |
| **Media Library** | `storage/app/media/` (or a configured disk like S3) | Filesystem/bucket copy | No database records for media files — only path-based references in content fields |
| **Localized Content** | `rainlab_translate_attributes` and `rainlab_translate_indexes` tables (RainLab.Translate) | `mysqldump` targeted tables | Shadow tables for translated attributes are invisible unless you query them explicitly |
| **Backend Users & Permissions** | `backend_users`, `backend_user_roles`, `backend_user_groups` | `mysqldump` | Passwords are Bcrypt-hashed; cannot export cleartext credentials |
| **Frontend Users** | `users` table (via RainLab.User or similar) | `mysqldump` / CSV export | Password hashes can be ported to other Laravel/PHP apps but not to non-Bcrypt systems |
| **System & Plugin Settings** | `system_settings` table + `System\Models\SettingModel` | `mysqldump` | Values are often serialized PHP or JSON — may need deserialization. Plugin settings live in the DB, not config files |
| **Multisite Content** | Site-scoped records with `site_id` columns | Export per site context | A naïve full-table export mixes data from all sites or locales together |
| **Soft-Deleted Records** | Same tables as live records, `deleted_at IS NOT NULL` | Filter on `deleted_at IS NULL` during import | `mysqldump` exports deleted records — non-Laravel import targets may ingest them as live content |

## Method 1: Direct Database and Filesystem Export

Since October CMS is self-hosted, **`mysqldump` (or `pg_dump` for PostgreSQL) is the fastest path to a full data export**. This is the method the official October CMS upgrade guide recommends for backups before major version changes.

```bash
# MySQL / MariaDB
mysqldump -u root -p database_name > october_export.sql

# PostgreSQL
pg_dump -U username -d database_name -F c -f october_export.dump

# SQLite
sqlite3 storage/database.sqlite .dump > october_export.sql
```

### What you get

- All plugin tables (blog posts, products, custom content)
- All Tailor content tables
- `system_files` metadata (file paths, attachment mappings)
- Backend and frontend users, roles, groups
- System and plugin settings
- Migration version history (`system_plugin_history`, `system_plugin_versions`)
- Soft-deleted records (rows where `deleted_at IS NOT NULL`) — filter these during import

### What you do not get

- **Actual files on disk** — `system_files` stores metadata and relative paths, not binaries. You must separately copy `storage/app/uploads/` and `storage/app/media/`.
- **Theme template files** — pages, layouts, partials live in `themes/your-theme/` as flat `.htm` files.
- **Plugin source code** — lives in `plugins/`. Useful for understanding schemas, not needed for data import.
- **Blueprint YAML files** — Tailor blueprints in `app/blueprints/` or `themes/your-theme/blueprints/` define the content structure.

> [!WARNING]
> **Do not skip the filesystem.** A database dump alone is incomplete. A common mistake is dumping only the DB, then discovering all images are broken because the physical files were never copied. You need `storage/app/`, `themes/`, and your blueprint files for a full export.

### Verify your export with row counts

Before moving on, reconcile row counts between your source database and exported files. Run this against your source database and again after import to confirm completeness:

```sql
-- Row count reconciliation across key tables
SELECT 'system_files' AS table_name, COUNT(*) AS row_count FROM system_files
  WHERE deleted_at IS NULL
UNION ALL
SELECT 'rainlab_blog_posts', COUNT(*) FROM rainlab_blog_posts
  WHERE deleted_at IS NULL
UNION ALL
SELECT 'backend_users', COUNT(*) FROM backend_users
  WHERE deleted_at IS NULL
UNION ALL
SELECT 'system_settings', COUNT(*) FROM system_settings;
```

Save these counts. On the target, run the same query after import and diff the results. Unexplained discrepancies indicate missing tables, incomplete dumps, or soft-deleted records being imported as live data.

### Selective table export for large databases

If you only need specific plugin data — for example, blog posts for migration to another CMS — export targeted tables:

```bash
mysqldump -u root -p database_name \
  rainlab_blog_posts \
  rainlab_blog_categories \
  system_files \
  > blog_export.sql
```

Always include `system_files` when exporting any content that has file attachments. The polymorphic relationship means all attachment metadata is centralized in that single table.

### Run from CLI, not the browser

For large exports, always run from the command line. PHP's default `max_execution_time` is 30 seconds for web requests but unlimited (0) on CLI. Browser-triggered exports fail long before the data is complete on anything beyond trivial datasets. ([php.net](https://www.php.net/manual/en/info.configuration.php))

Treat the SQL dump as an archival source of truth, not as a clean migration payload. You will still need transformation scripts to map October's schema to your target platform.

## Method 2: Built-in Backend Export via ImportExportController

October CMS ships with a **`Backend\Behaviors\ImportExportController`** that plugins can implement to offer export functionality from the admin panel. This is not a platform-wide feature — it is opt-in per plugin. ([docs.octobercms.com](https://docs.octobercms.com/4.x/extend/importexport/importexport-controller.html))

### How it works

A plugin developer creates an export model extending `Backend\Models\ExportModel` and implements the `exportData()` method. The admin panel then provides a UI where users can select columns and download a file.

```php
class PostExport extends \Backend\Models\ExportModel
{
    public function exportData($columns, $sessionKey = null)
    {
        $posts = Post::all();
        $posts->each(function($post) use ($columns) {
            $post->addVisible($columns);
        });
        return $posts->toArray();
    }
}
```

To add this behavior to any backend controller:

```php
namespace Acme\MyPlugin\Controllers;

class MyData extends \Backend\Classes\Controller
{
    public $implement = [
        \Backend\Behaviors\ImportExportController::class,
    ];

    public $importExportConfig = 'config_import_export.yaml';
}
```

In the configuration YAML:

```yaml
export:
    title: Export My Data
    modelClass: Acme\MyPlugin\Models\MyDataExport
    list: $/acme/myplugin/models/mydata/columns.yaml
```

In October CMS 4.x, the `defaultFormatOptions.fileFormat` setting supports `json`, `csv`, and `csv_custom`. Older versions (2.x, 3.x) default to CSV only, so check your installed version before assuming JSON export is available. ([docs.octobercms.com](https://docs.octobercms.com/4.x/extend/importexport/importexport-controller.html))

### Limitations

- **Only available if the plugin implements it.** Many third-party plugins do not include export support.
- **No automatic relationship resolution.** A blog post with a `belongsTo` category relationship exports the foreign key ID, not the category name, unless the developer explicitly handles it.
- **File attachments export as nothing.** Polymorphic file relations (`$attachOne`, `$attachMany`) are not included — no file URLs, no paths.
- **No pagination or streaming for large datasets.** The `exportData()` method loads all records into memory. Tables with 100K+ rows can exhaust PHP's memory limit.
- **Relational data flattens.** A post with multiple categories becomes a comma-separated string. Reconstructing many-to-many relationships from a flat file is error-prone.

> [!NOTE]
> **The RainLab.Blog plugin** — the most widely used October CMS content plugin — does implement import/export. But the export only covers post metadata and body content, not attached images or relational category hierarchies in a structured format.

## Method 3: Custom API Endpoints or Laravel Routes

October CMS **does not ship with a built-in REST API for content**. There is no `/api/posts` endpoint out of the box. If you need programmatic data extraction — to feed a migration pipeline or sync to another system — you build it yourself.

Because October runs on Laravel, you have multiple options.

### Option A: Laravel routes in a plugin

Define standard routes in a plugin's `routes.php`:

```php
// plugins/acme/api/routes.php
use Tailor\Models\EntryRecord;
use Illuminate\Support\Facades\Route;

Route::get('api/v1/export/articles', function () {
    $posts = EntryRecord::inSection('Blog\Post')
        ->with('cover_image')
        ->paginate(100);

    return response()->json($posts);
});
```

### Option B: CMS pages as JSON endpoints

October CMS lets you create API endpoints using CMS pages that return JSON instead of HTML. The official docs describe using layouts as shared middleware for authentication and throttling. ([docs.octobercms.com](https://docs.octobercms.com/4.x/cms/resources/building-apis.html))

```twig
url = "/api/posts"
layout = "api"
==
<?php
function onStart()
{
    $posts = \RainLab\Blog\Models\Post::with(['categories', 'featured_images'])
        ->published()
        ->get();
    
    return response()->json($posts);
}
==
```

### Option C: Third-party API plugins

Plugins like **Simple API** and **API Generator** can auto-generate REST endpoints from your models. These work but carry caveats:

- **Maintenance risk** — community plugins may lag behind major October CMS versions.
- **No built-in rate limiting** unless you add middleware.
- **Basic authentication only** in most cases.

### Server-level constraints

Because you host the application, there are no vendor API limits. Your constraints are all server-level:

- **PHP `memory_limit`:** Typically 128MB or 256MB. Loading thousands of Eloquent models into memory causes fatal errors. Always use `paginate()` or `chunk()`.
- **PHP `max_execution_time`:** 30–60 seconds for web requests. Complex transformations (like Markdown-to-HTML parsing) before returning JSON may time out.
- **Nginx/Apache timeouts:** Your web server or load balancer may drop the connection after 60 seconds even if PHP is configured for longer.

## Method 4: Artisan Console Commands (Scripted Export)

For migration exports, a custom artisan command is often the most practical approach. It runs on CLI (no execution time limit), can resolve polymorphic relationships, handles pagination with `chunk()`, and produces structured output you can run repeatedly during testing.

The artisan approach is the most migration-relevant method for sites with complex data. Here is a complete example that handles polymorphic file resolution, soft-delete filtering, and chunked processing to avoid memory exhaustion:

```php
namespace Acme\Export\Console;

use Illuminate\Console\Command;
use RainLab\Blog\Models\Post;

class ExportPosts extends Command
{
    protected $name = 'export:posts';
    protected $description = 'Export all published blog posts to JSON with resolved file attachments';

    public function handle()
    {
        $exportDir = storage_path('app/export');
        if (!is_dir($exportDir)) {
            mkdir($exportDir, 0755, true);
        }

        $total = 0;

        // chunk() processes records in batches of 200 to avoid memory exhaustion
        Post::with(['categories', 'featured_images'])
            ->whereNull('deleted_at')          // exclude soft-deleted records
            ->chunk(200, function ($posts) use ($exportDir, &$total) {
                foreach ($posts as $post) {
                    $data = $post->toArray();

                    // Resolve polymorphic file attachments to actual paths
                    if ($post->featured_images->count()) {
                        $data['image_urls'] = $post->featured_images
                            ->map(fn($img) => $img->getPath())
                            ->toArray();

                        // Also capture disk_name for filesystem mapping during import
                        $data['image_disk_names'] = $post->featured_images
                            ->map(fn($img) => $img->disk_name)
                            ->toArray();
                    }

                    // Strip Twig shortcodes and parse Markdown to HTML if needed
                    // $data['content_html'] = (new \Markdown\Parser)->parse($post->content);

                    $filename = "{$exportDir}/post-{$post->id}.json";
                    file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
                    $total++;
                }
            });

        $this->info("Exported {$total} posts to {$exportDir}.");
    }
}
```

Key decisions in this implementation:

- **`whereNull('deleted_at')`** — explicitly excludes soft-deleted posts. Remove this only if your migration target needs a full history.
- **`chunk(200, ...)`** — processes 200 records at a time. Adjust downward if posts have many eager-loaded relationships.
- **`disk_name` capture** — preserves the hashed filename used to locate the physical file in `storage/app/uploads/`, making file remapping on the target deterministic.
- **Markdown-to-HTML comment** — if your target platform expects HTML rather than Markdown ([Webflow](https://clonepartner.com/blog/blog/how-to-export-data-from-webflow-methods-api-limits-portability/), Contentful, Gutenberg), uncomment and add a Parsedown step. October CMS uses Parsedown internally for Markdown rendering.

> [!TIP]
> **For one-time migration exports**, do not over-engineer a REST API. A quick artisan command that queries the database and writes JSON files to disk is faster and more reliable than building and securing a full API you will use once.

## Exporting Tailor Content

**Tailor** — introduced in October CMS v3 — lets you define content structures via YAML blueprints without building a full plugin. Tailor content lives in auto-generated database tables, and extracting it requires understanding the mapping between blueprints and tables.

### The challenge

Tailor table names are generated from blueprint UUIDs, not human-readable names. You cannot look at the database schema and immediately know which table maps to which content type. To export Tailor data cleanly:

1. **Read the blueprint YAML files** from `app/blueprints/` or `themes/your-theme/blueprints/`
2. **Query using Tailor's model API** with human-readable handles rather than raw table names

```php
use Tailor\Models\EntryRecord;
use Tailor\Models\GlobalRecord;

$posts = EntryRecord::inSection('Blog\Post')->get();
$posts->load(['categories', 'author']);

file_put_contents(
    storage_path('app/exports/blog-posts.json'),
    json_encode($posts->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);

$config = GlobalRecord::findForGlobal('Blog\Config');
file_put_contents(
    storage_path('app/exports/blog-config.json'),
    json_encode($config?->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
```

October documents `EntryRecord`, `StructureRecord`, `StreamRecord`, `SingleRecord`, and `GlobalRecord` for querying Tailor content by handle. Always use these models instead of hard-coding UUID-derived table names. ([docs.octobercms.com](https://docs.octobercms.com/4.x/cms/tailor/models.html))

> [!WARNING]
> **Tailor never drops columns.** If you remove a field from a blueprint, the database column is renamed with a prefix, not deleted. Exported Tailor tables may contain orphaned columns that no longer correspond to any active blueprint field. Filter your export against current blueprint definitions — not every column in the raw table. Run `php artisan tailor:prune` to clean up orphaned columns before exporting if possible.

## The system_files Problem: Exporting File Attachments

This is where most October CMS exports go wrong.

All file attachments — regardless of which plugin uploaded them — are tracked in a single `system_files` table using **polymorphic relationships**. Understanding this table's schema is essential for any migration that must preserve file associations.

### system_files schema (key columns)

| Column | Type | Description |
|---|---|---|
| `id` | int | Primary key |
| `attachment_type` | varchar | Fully-qualified model class (e.g., `RainLab\Blog\Models\Post`) |
| `attachment_id` | int | ID of the parent record |
| `field` | varchar | Relationship name on the model (e.g., `featured_images`) |
| `disk_name` | varchar | Hashed filename on disk (e.g., `5f3a2b1c.jpg`) — used to locate the physical file |
| `file_name` | varchar | Original upload filename |
| `file_size` | int | File size in bytes |
| `content_type` | varchar | MIME type |
| `sort_order` | int | Position when multiple files are attached to one field |
| `is_public` | tinyint | 1 = `storage/app/uploads/public/`, 0 = `storage/app/uploads/protected/` |
| `created_at` | timestamp | Upload timestamp |

The **actual files** live in `storage/app/uploads/public/` (when `is_public = 1`) or `storage/app/uploads/protected/` (when `is_public = 0`), organized into partition subdirectories derived from the first few characters of `disk_name`. A file with `disk_name = 5f3a2b1c.jpg` lives at `storage/app/uploads/public/5f/3a/5f3a2b1c.jpg`.

**Protected files** are served through October's routing layer with access checks — they are not publicly accessible by direct URL. When migrating protected files to a non-October platform, you must decide whether to make them public, implement equivalent access control, or serve them from a different authenticated endpoint.

### How to export attachments correctly

1. **Dump the `system_files` table** to preserve all relationship mappings
2. **Copy the entire `storage/app/uploads/` directory** (both `public/` and `protected/`)
3. **During import to the target platform**, use `attachment_type` + `attachment_id` + `field` to map files back to their parent records

```sql
-- Find all images attached to blog posts, with disk path reconstruction
SELECT
    sf.id,
    sf.disk_name,
    sf.file_name,
    sf.file_size,
    sf.content_type,
    sf.attachment_id,
    sf.field,
    sf.sort_order,
    sf.is_public,
    CONCAT(
        CASE WHEN sf.is_public = 1 THEN 'storage/app/uploads/public/' ELSE 'storage/app/uploads/protected/' END,
        SUBSTRING(sf.disk_name, 1, 2), '/',
        SUBSTRING(sf.disk_name, 3, 2), '/',
        sf.disk_name
    ) AS local_path
FROM system_files sf
WHERE sf.attachment_type = 'RainLab\\Blog\\Models\\Post'
ORDER BY sf.attachment_id, sf.sort_order;
```

**Media Library files** work differently. Images added via the Media Manager (not the File Upload widget) are just files in `storage/app/media/` with no database records. References in content fields are stored as relative paths like `/media/my-image.jpg`. If your media library is configured to use S3 or another external disk, the files are not on the app server at all.

### Exporting S3-backed media

If your installation uses an S3-compatible storage driver for media, the files are not on the application server. Use the AWS CLI to enumerate and download bucket contents:

```bash
# List all media files
aws s3 ls s3://your-bucket/media/ --recursive

# Sync entire media directory to local for export
aws s3 sync s3://your-bucket/media/ ./media-export/

# Sync uploads (file attachments)
aws s3 sync s3://your-bucket/uploads/ ./uploads-export/
```

The bucket path structure depends on your `config/filesystems.php` configuration. Verify the `root` path for your configured disk before running sync commands.

> [!WARNING]
> If attachments use an external storage driver, October's `getLocalPath()` method downloads a temporary local copy for scripting. Convenient for extraction, but the canonical binary may not live on the application server. For bulk exports, use the storage provider's native tools (AWS CLI, rclone) rather than iterating `getLocalPath()` per record. ([docs.octobercms.com](https://docs.octobercms.com/3.x/extend/database/attachments.html))

## Exporting Multilingual Content (RainLab.Translate)

Sites using **RainLab.Translate** store translated content in separate shadow tables that are invisible to a naïve database export workflow.

RainLab.Translate uses two key tables:

- **`rainlab_translate_attributes`** — stores translated field values as JSON, keyed by `model_id`, `model_type`, and `locale`
- **`rainlab_translate_indexes`** — stores individual translated field values for query indexing

When exporting a multilingual site, always include these tables explicitly:

```bash
mysqldump -u root -p database_name \
  rainlab_blog_posts \
  rainlab_blog_categories \
  rainlab_translate_attributes \
  rainlab_translate_indexes \
  system_files \
  > multilingual_export.sql
```

To reconstruct translated content per locale during migration:

```php
use RainLab\Translate\Models\Attribute;

// Get all translations for blog posts in French
$translations = Attribute::where('model_type', 'RainLab\Blog\Models\Post')
    ->where('locale', 'fr')
    ->get()
    ->keyBy('model_id');

// Merge translations onto post records
$posts = \RainLab\Blog\Models\Post::whereNull('deleted_at')->get();

foreach ($posts as $post) {
    $translated = $translations->get($post->id);
    $post->fr_title = $translated ? ($translated->attribute_data['title'] ?? $post->title) : $post->title;
    // ... map other translated fields
}
```

If your migration target uses locale-specific records rather than an attribute table (WordPress WPML, Contentful locale variants), you will need to denormalize this data — one record per locale per content item — rather than keeping the translated attributes in a separate table.

## Theme Files, Database-Driven Templates, and Static Pages

October CMS themes are flat files. Pages, layouts, partials, and content blocks are `.htm` files in `themes/your-theme/`. A simple `tar` or `rsync` handles the export:

```bash
tar -czf theme-export.tar.gz themes/your-theme/
```

But watch for these complications:

- **Database-driven themes:** Some deployments push theme data to the database for multi-server setups (configured in `config/cms.php` with `databaseTemplates`). If enabled, the filesystem copy is stale — the live templates are in the database. Run `php artisan theme:copy <theme> --import-db` to sync database templates back to disk before exporting. ([docs.octobercms.com](https://docs.octobercms.com/3.x/cms/themes/database-themes.html))
- **Static Pages plugin** (RainLab.Pages) stores page content as flat files in `themes/your-theme/content/static-pages/`, but menu structures may live in the database.
- **Multisite themes:** A multisite install may assign different themes per site. Export all active themes, not just one.

## Backup Plugins: Spatie-Based Full Export

For full-site archival (not migration-ready, but useful for backups), the **panakour/backup** plugin wraps Laravel's `spatie/laravel-backup` package. It creates full backups including database and files, outputs as a ZIP, and can store to local disk or external services.

```bash
php artisan backup:run            # Full backup (files + DB)
php artisan backup:run --only-db  # Database only
```

Limitations: requires `mysqldump` (or equivalent) on the server, not compatible with Windows servers per the plugin's own docs, and produces a backup archive — not a structured, migration-ready export.

## Recreating the Schema on a Clean Target

If you are migrating October CMS to a fresh server rather than [moving data to a different platform](https://clonepartner.com/blog/blog/october-cms-vs-plone-2026-architecture-tco-migration-guide/), use `october:up` to recreate the schema before importing your dump:

```bash
# On the target server (fresh October installation)
php artisan october:up
```

This runs October's own migration system, which differs from plain `artisan migrate` in that it also seeds plugin version history (`system_plugin_history`, `system_plugin_versions`) and handles October-specific schema bootstrapping. Running `artisan migrate` alone may leave the plugin registry in an inconsistent state, causing plugin updates to fail or re-run migrations that already exist in your imported dump.

After `october:up`, import your SQL dump. If you encounter duplicate migration errors, it means `october:up` applied migrations that your dump also contains — you may need to truncate `system_plugin_history` from your dump or import selectively.

## Common Export Traps That Break Migrations

- **Exporting themes without checking for database-driven templates.** The live version may be in the DB, not on disk.
- **Dumping `system_files` rows without copying `uploads/public` and `uploads/protected`.** Metadata without binaries is useless.
- **Importing soft-deleted records as live content.** `mysqldump` exports all rows. Non-Laravel targets do not understand `deleted_at`. Always filter `WHERE deleted_at IS NULL` during transformation.
- **Missing RainLab.Translate shadow tables.** A multilingual site export that omits `rainlab_translate_attributes` and `rainlab_translate_indexes` loses all non-default locale content.
- **Assuming every Tailor column is current.** Old fields and tables linger until pruned.
- **Flattening relational data to CSV too early.** You lose foreign keys, hierarchy, and site context that matter during target mapping.
- **Forgetting plugin settings in the database.** Settings stored via `System\Models\SettingModel` are in the DB, not config files. ([docs.octobercms.com](https://docs.octobercms.com/4.x/extend/settings/model-settings.html))
- **Mixing multisite data.** A full-table export without filtering by `site_id` blends content from different sites or locales. October's `Site::withContext(...)` service lets you scope exports per site. ([docs.octobercms.com](https://docs.octobercms.com/4.x/extend/services/site.html))
- **Neglecting content formatting.** October CMS uses Markdown and Twig-based shortcodes (especially in RainLab plugins). If the target platform expects HTML or block-based JSON (Webflow, Contentful, WordPress Gutenberg), you need a transformation step: Markdown parsing (October uses Parsedown internally), shortcode/Twig stripping, and URL rewriting.
- **Serialized PHP data.** Older plugins store settings and multi-select fields as PHP serialized strings (e.g., `a:2:{i:0;s:4:"value";}`). Non-PHP migration targets need a deserialization step before import.
- **Protected file accessibility.** Files in `uploads/protected/` are served through October's auth layer. When migrating to a platform without equivalent access control, determine before migration whether these files should become public or require a new access mechanism.

## Version-Specific Gotchas

- **v2.x → v3.x:** Tailor did not exist before v3.x. If your site runs v2.x, there are no Tailor tables to worry about.
- **v3.x → v4.x:** v4.0 requires PHP 8.2+ and Laravel 12. Plugin compatibility may break. Always test data exports against the actual installed version.
- **`ImportExportController` JSON format:** Available only in v4.x. Sites on v2.x or v3.x are CSV-only for backend exports.
- **`databaseTemplates`:** The config key has been consistent across versions, but the artisan sync command (`theme:copy --import-db`) may behave differently in v2.x. Test before relying on it.

## Export Checklist for Migration

- [ ] **Database dump** — full `mysqldump` or `pg_dump`
- [ ] **Row count reconciliation** — run the count query before export; re-run after import and diff
- [ ] **`storage/app/uploads/`** — all file attachments (both `public/` and `protected/` subdirectories)
- [ ] **`storage/app/media/`** — all media library files (or sync from configured media disk/bucket)
- [ ] **`themes/your-theme/`** — all CMS templates, pages, layouts, partials
- [ ] **`app/blueprints/`** and theme-level blueprints — Tailor schema definitions (v3+)
- [ ] **`plugins/`** — plugin source (needed to understand schemas, not for import)
- [ ] **`system_files` mapping** — exported separately as CSV/JSON for attachment resolution, with `disk_name`-to-path reconstruction
- [ ] **`rainlab_translate_attributes` and `rainlab_translate_indexes`** — if using RainLab.Translate
- [ ] **Soft-deleted records filtered** — verify `deleted_at IS NULL` applied during transformation
- [ ] **Database-driven themes synced** — run `theme:copy --import-db` if `databaseTemplates` is enabled
- [ ] **S3/external media exported** — use provider's native CLI tools if media is not on the app server
- [ ] **Multisite context verified** — export per-site if using multisite
- [ ] **Plugin-specific exports checked** — verify if critical plugins (Blog, Pages, Translate) have their own export tools
- [ ] **Protected files access plan confirmed** — determine how `uploads/protected/` files will be served on the target

## When to Skip DIY and Get Help

October CMS exports are straightforward if you are comfortable with Laravel, Eloquent, and direct database access. Complexity multiplies fast when:

- You have **10+ plugins** with interconnected data models
- **Tailor content** uses nested blueprints with cross-references
- **File attachments span thousands of records** across multiple plugins and need remapping to a different CMS's file system
- You need to **transform October CMS's data model** into a different platform's schema (WordPress, Webflow, a headless CMS)
- **Multisite data** needs to be separated or merged during export
- **Multilingual content** in RainLab.Translate must be denormalized into per-locale records for the target platform
- **Content formatting** requires Markdown-to-HTML parsing, shortcode stripping, and URL rewriting across thousands of records
- **Protected file assets** require access control decisions that differ from October's approach

The export is the straightforward part. The hard part is making it import-ready for the target platform — untangling polymorphic file relationships, filtering soft-deleted records, deserializing PHP arrays, reconstructing translated content per locale, mapping dynamic Tailor blueprints to a fixed schema, and rewriting content references.

> Migrating away from October CMS? ClonePartner handles the database queries, file mapping, content transformation, and schema translation — delivering clean, import-ready data in days, not weeks.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Does October CMS have a built-in data export feature?

October CMS has no platform-wide export button. It ships with a Backend ImportExportController that plugins can implement to offer CSV or JSON exports for their specific data. Each plugin must opt in, and many don't. For a full export, you need a database dump plus filesystem copies.

### How do I export file attachments from October CMS?

File attachments are tracked in the system_files table via polymorphic relationships (attachment_type, attachment_id, field columns). The actual files live in storage/app/uploads/. Export the system_files table to preserve mappings, then copy the entire uploads directory. You need both to reconstruct file-to-record relationships in the target platform.

### How do I export Tailor content from October CMS?

Tailor content is stored in auto-generated database tables with names derived from blueprint UUIDs. Use Tailor's model API (EntryRecord::inSection(), GlobalRecord::findForGlobal()) to query by human-readable handle, then serialize to JSON. Include the blueprint YAML files from app/blueprints/ to document the schema.

### Can I use an API to export October CMS content?

October CMS does not include a REST API for content out of the box. You can build custom endpoints using Laravel routes or CMS pages, or install community plugins like Simple API. For one-time migration exports, a custom artisan command that writes JSON to disk is faster and more reliable.

### Are there API rate limits when exporting from October CMS?

There are no vendor-imposed rate limits because October CMS is self-hosted. Your practical limits are server-level: PHP memory_limit, max_execution_time (30s default for web requests, unlimited on CLI), and Nginx/Apache connection timeouts.
