A powerful DatoCMS plugin that allows you to export your project's records and assets directly from the dashboard. Whether you need a complete backup, a specific set of data for analysis, or just a single record, Project Exporter handles it with support for multiple popular formats.

manifest, schema ID/API-key maps, projectConfiguration (site/settings resources), and a referenceIndex for records/uploads/blocks/structured-text links.manifest.json and deterministic filename conventions for easier re-import.datocms-plugin-project-exporter.Once installed, you can configure the default export format in the plugin settings:
JSONCSVXMLXLSXNote: You can also change the format on-the-fly when performing an export.
To perform bulk exports, navigate to the plugin's configuration screen (typically found under Settings > Plugins > Project Exporter > Config Screen or the dedicated plugin page if applicable).
manifest.json with source upload IDs and metadatau_<sourceUploadId>__<sanitizedOriginalFilename>When editing a specific record:
This plugin is built with React and the DatoCMS Plugin SDK. To contribute or modify the plugin locally:
git clone https://github.com/marcelofinamorvieira/datocms-plugin-project-exporter.gitnpm install# orpnpm installnpm startdatocms-plugin-sdk, datocms-react-uijson-2-csv (CSV generation)jsontoxml (XML generation)exceljs (Excel generation)jszip (Asset zipping)This section documents the concrete output contract of this plugin so you can build an importer with predictable behavior.
All record exports download with this filename pattern:
allDatocmsRecords<ISO_TIMESTAMP>.<extension>Examples:
allDatocmsRecords2026-02-10T18:12:33.271Z.jsonallDatocmsRecords2026-02-10T18:12:33.271Z.csvNotes:
JSON exports contain the full envelope described below (manifest, schema, projectConfiguration, referenceIndex, etc.).CSV, XML, and XLSX exports contain only the exported record data (no manifest/schema/reference index).Assets are split into one or more ZIP files using this naming template:
allAssets.part-<PPP>-of-<TTT>.<timestamp>.zipPPP and TTT are zero-padded to 3 digits (001, 012, etc.).timestamp is generated from new Date().toISOString().replace(/:/g, '-').allAssets.part-003-of-012.2026-02-09T12-00-00.000Z.zip.Each binary asset inside a ZIP uses:
u_<sourceUploadId>__<sanitizedOriginalFilename>Sanitization rules:
sourceUploadId:
_[A-Za-z0-9_-] with --unknown if emptyoriginalFilename:
_[A-Za-z0-9._-] with --file if emptyExample:
upload:123 + Hero Image (Final).pngu_upload-123__Hero_Image_-Final-.pngmanifest.json Inside Each Asset ZIPEvery ZIP contains a manifest.json at the root with this structure:
type AssetZipManifest = { manifestVersion: "2.0.0"; generatedAt: string; // ISO timestamp chunk: { index: number; // 1-based chunk index totalChunks: number; filename: string; // actual ZIP filename for this chunk assetCount: number; estimatedBytes: number; // conservative estimate used for chunking }; conventions: { zipEntryName: "u_<sourceUploadId>__<sanitizedOriginalFilename>"; zipFilename: "allAssets.part-{part}-of-{total}.{timestamp}.zip"; }; limits: { maxZipBytes: 157286400; // 150 * 1024 * 1024 maxFilesPerZip: 100; sizeSafetyFactor: 1.2; }; assets: AssetManifestEntry[];};
type AssetManifestEntry = { sourceUploadId: string; zipEntryName: string; originalFilename: string; size: number | null; mimeType: string | null; width: number | null; height: number | null; checksum: string | null; // upload md5 url: string | null; path: string | null; metadata: { // only included if present on the source upload: default_field_metadata?: unknown; field_metadata?: unknown; custom_data?: unknown; tags?: unknown; notes?: unknown; author?: unknown; copyright?: unknown; focal_point?: unknown; is_image?: unknown; blurhash?: unknown; };};.json record exports)JSON record exports (bulk and single-record) use this envelope:
type RecordExportEnvelope = { manifest: { exportVersion: "2.1.0"; pluginVersion: string; exportedAt: string; // ISO timestamp sourceProjectId: string | null; sourceEnvironment: string | null; defaultLocale: string | null; locales: string[]; scope: "bulk" | "single-record"; filtersUsed: { modelIDs?: string[]; textQuery?: string; }; configurationExport: { includedResources: ( | "site" | "scheduledPublications" | "scheduledUnpublishings" | "fieldsets" | "menuItems" | "schemaMenuItems" | "modelFilters" | "plugins" | "workflows" | "roles" | "webhooks" | "buildTriggers" )[]; warningCount: number; }; }; schema: { itemTypes: Record<string, unknown>[]; // raw itemTypes from CMA fields: Record<string, unknown>[]; // raw fields from CMA itemTypeIdToApiKey: Record<string, string>; // model id -> api_key fieldIdToApiKey: Record<string, string>; // field id -> api_key fieldsByItemType: Record< string, { fieldId: string; apiKey: string; fieldType: string; localized: boolean; }[] >; }; projectConfiguration: { site: Record<string, unknown> | null; // full Site payload from CMA scheduledPublications: { itemId: string; itemTypeId: string | null; scheduledAt: string; currentVersion: string | null; }[]; scheduledUnpublishings: { itemId: string; itemTypeId: string | null; scheduledAt: string; currentVersion: string | null; }[]; fieldsets: Record<string, unknown>[]; menuItems: Record<string, unknown>[]; schemaMenuItems: Record<string, unknown>[]; modelFilters: Record<string, unknown>[]; plugins: Record<string, unknown>[]; workflows: Record<string, unknown>[]; roles: Record<string, unknown>[]; webhooks: Record<string, unknown>[]; buildTriggers: Record<string, unknown>[]; warnings: { resource: string; message: string; }[]; }; records: Record<string, unknown>[]; // raw records from CMA iterator referenceIndex: { recordRefs: RecordReference[]; uploadRefs: UploadReference[]; structuredTextRefs: StructuredTextReference[]; blockRefs: BlockReference[]; }; assetPackageInfo: { packageVersion: "2.0.0"; zipNamingConvention: "allAssets.part-{part}-of-{total}.{timestamp}.zip"; zipEntryNamingConvention: "u_<sourceUploadId>__<sanitizedOriginalFilename>"; manifestFilename: "manifest.json"; chunkingDefaults: { maxZipBytes: 157286400; maxFilesPerZip: 100; sizeSafetyFactor: 1.2; }; lastAssetExportSnapshot: LastAssetExportSnapshot | null; };};
type LastAssetExportSnapshot = { packageVersion: string; generatedAt: string; chunkFilenames: string[]; totalChunks: number; totalAssets: number; maxZipBytes: number; maxFilesPerZip: number; sizeSafetyFactor: number;};Where references are:
type BaseRef = { recordSourceId: string; sourceBlockId: string | null; fieldApiKey: string; locale: string | null; jsonPath: string;};
type RecordReference = BaseRef & { targetSourceId: string; kind: string;};
type UploadReference = BaseRef & { targetSourceId: string; kind: string;};
type StructuredTextReference = BaseRef & { targetSourceId: string; targetType: "record" | "block"; kind: "link" | "block";};
type BlockReference = BaseRef & { blockSourceId: string; blockModelId: string | null; parentBlockSourceId: string | null; kind: string; synthetic: boolean;};manifest Behavior Detailsmanifest.exportVersion is currently fixed at 2.1.0.manifest.pluginVersion comes from:
REACT_APP_PLUGIN_VERSION, elsenpm_package_version, else"1.0.0".manifest.scope:
bulk for config-screen bulk exportssingle-record for sidebar single-record exportmanifest.filtersUsed:
modelIDs and/or textQuery{}.sourceProjectId, sourceEnvironment, defaultLocale, locales) are derived from projectConfiguration.site; if site fetch fails they fall back to null / [].manifest.configurationExport.includedResources lists all configuration collections shipped in projectConfiguration.manifest.configurationExport.warningCount is the number of non-fatal resource fetch failures collected in projectConfiguration.warnings.projectConfiguration.site is the full raw site resource payload.scheduledPublications / scheduledUnpublishings are derived from exported records (meta.publication_scheduled_at / meta.unpublishing_scheduled_at) because CMA does not expose a global list endpoint for those resources.fieldsets are collected across all exported models/blocks.modelFilters comes from CMA item_type_filter resources.projectConfiguration are present even when empty.projectConfiguration.warnings stores per-resource fetch failures without aborting the export.jsonPath always points to the location in records where the relationship was found.$.records[index].jsonPath (for example $.records[0].content.en.document...)locale property ("en", "pt", etc.).sourceBlockId is null for top-level record fields and set for relationships found inside blocks.(context + target + kind) is emitted once.kind values currently emitted by this version:
recordRefs.kind: link, links, structured_text_itemLink, structured_text_inlineItem, structured_text_links_array, unknown_itemuploadRefs.kind: file, gallery, unknown_uploadblockRefs.kind: modular_content, single_block, nested_block, structured_text_block, structured_text_blocks_arrayWhen a block-like object has no id, the exporter creates a synthetic block ID:
synthetic::<recordSourceId>::<jsonPath>This appears in blockRefs.blockSourceId with synthetic: true.
If you are writing an importer, this order is reliable for the current contract:
schema maps (itemTypeIdToApiKey, fieldIdToApiKey, fieldsByItemType) and projectConfiguration.site, menu/schema menu items, workflows, roles, webhooks, build triggers, etc.).sourceUploadId -> targetUploadId mapping from each ZIP manifest.json.sourceRecordId -> targetRecordId map.referenceIndex.structuredTextRefs and blockRefs.This project is licensed under the MIT License.