The Complete Guide to Salesforce File Migration Between Orgs
Moving Salesforce files between organizations sounds straightforward — until you actually try to do it. Salesforce provides no native bulk file transfer tool, and the file data model has accumulated enough complexity over the years that a "simple" migration becomes a project of its own. This guide walks through the full Salesforce file migration process: the file model, the eight steps of a clean migration, the failure modes you'll hit, and how to verify the result.
Why Salesforce file migration is harder than it looks
If you've moved data between Salesforce orgs before — Accounts, Contacts, Opportunities — you've likely used Data Loader, a CSV pipeline, or a tool like Workbench. Records are copy-pasteable and well-understood. Files are a different problem.
Salesforce stores files as binary blobs attached to records, but the storage model is layered. A file isn't a single row in a table — it's typically a ContentDocument wrapping one or more ContentVersion records, joined to parent records (Accounts, Cases, Opportunities) through ContentDocumentLink rows. Older files might be Attachment records that don't follow this model at all. To migrate a file you have to download the binary content, recreate it in the target org, and re-establish the parent-record links — without losing metadata along the way.
Add API rate limits, file size caps, sharing visibility, and the fact that the target org probably doesn't have the parent records yet, and a "simple file migration" turns into a multi-week project that fails in non-obvious ways.
The Salesforce file model in 60 seconds
Four objects matter for migrations:
Attachment(ID prefix00P) — the legacy file model. One Attachment belongs to exactly one parent record. No version history. Limited metadata. Salesforce stopped recommending these around 2016 but most older orgs still have plenty.ContentDocument(ID prefix069) — the modern file container. OneContentDocumentrepresents the file as a logical entity. It doesn't itself store the binary.ContentVersion(ID prefix068) — the actual binary data plus version metadata. EveryContentDocumenthas at least oneContentVersion, and may have many (one per uploaded version).ContentDocumentLink— a junction object joining aContentDocumentto one or more parent records. This is how a single file can be attached to multiple records, and how sharing visibility is set per record.
For a deeper dive into how these objects relate, see our ContentVersion vs. Attachment guide.
Why Data Loader can't do file migration
Data Loader (and similar tools — Workbench, dataloader.io, the Bulk API directly) move records by reading and writing CSV. They don't move file binaries. You can export a list of ContentDocument IDs, but the actual file bytes live in ContentVersion.VersionData, which Data Loader doesn't download in any usable form.
The naive workaround is to query ContentVersion, retrieve VersionData via the REST API, and POST it back to the target org as a new ContentVersion — then create a ContentDocumentLink to associate it with the right parent. That works for a handful of files. It does not work for ten thousand. You hit API limits, file size limits, network timeouts, missing parents, and sharing rule violations — all without a way to track which files succeeded or recover from failures.
The 8-step file migration playbook
1. Inventory the source org
Before you migrate, count what's there. Run SOQL queries to get totals per file type per parent object. The attachment counting guide covers this in depth, but the headline numbers you need are: total ContentDocument count, total Attachment count, distribution by parent object type, and total file size. These shape every other decision.
2. Decide scope
Almost no migration moves every file. Common scoping decisions:
- Date range — only files created after January 1, 2022, or only files older than a retention threshold.
- Object type — Account files but not Case files, or only the custom objects associated with the migrating business unit.
- File type — modern
ContentDocuments only, leave legacyAttachments in place (or vice versa).
Write the scope down explicitly. Every later step reads back to this document.
3. Verify target org has parent records
A file without a parent is orphaned. If the target org doesn't have an Account with the same external ID as the file's source parent, the ContentDocumentLink can't be created — and a file with no link is invisible to users. Run your data migration first (see our Salesforce Data Migration tool for how to handle that part), then run file migration on top of the now-populated target.
4. Validate access
Two limits to check up front:
- API call limits — your target org has a daily API limit (typically 15,000 calls per Salesforce license, with a cap that varies by edition). A 50,000-file migration that uses one API call per file blows through that. Use Bulk API 2.0 where possible to batch requests.
- File size limits — single-file uploads via REST cap at 37 MB; via Bulk API at 2 GB; via the standard UI at 2 GB. Files above the API limits need different handling. Identify any oversize files early.
5. Choose the migration approach
For under 100 files, manual download/upload via the UI is faster than building tooling. For 100 to a few thousand, a scripted approach (Postman + REST API, or a custom Apex job) might suffice. Above that, you want a real tool — one that handles parallelism, retry logic, error recovery, progress tracking, and reporting.
6. Run a small dry-run first
Pick 50–100 representative files (different sizes, different parent objects, both ContentDocument and Attachment). Migrate them. Check the result in the target org. Verify file content opens, parent record associations are correct, sharing visibility is set as expected, and CreatedDate / OwnerId are reasonable.
The dry-run catches issues — missing fields, sharing rule conflicts, picklist mismatches, target org permission gaps — that would otherwise surface 30,000 files into the real run.
7. Execute with monitoring
Run the full migration with active monitoring. Watch progress: are files moving at the expected rate, are errors spiking, are API limits being approached? A long migration left running unattended is the most common reason a project slips by a week — silent failures aren't caught until the report is generated.
8. Verify and reconcile
After the run, compare counts: source ContentDocument count (in scope) vs target ContentDocument count for the migrated set. Spot-check a sample of records — do they have all the expected files? Are sharing rules correct? Is file content openable?
Hand the per-file CSV report to whoever needs sign-off on the migration. Archive both the report and the source-org snapshot for audit purposes.
Common failure modes
- Missing parent records — file migration started before data migration finished.
ContentDocumentLinkcreation fails because the parent doesn't exist in target. Fix: serialize the steps; data first, files second. - API limit exhaustion — naive per-file API calls blow through the daily allotment. Fix: use Bulk API 2.0 for high-volume operations; batch requests; use parallel workers respecting concurrency caps.
- Network timeouts on large files — single requests stall on multi-hundred-MB files. Fix: chunked upload via Bulk API; configurable timeout and retry policy.
- Sharing rule conflicts — target org has stricter sharing rules than source.
ContentDocumentLinkinsert fails for restricted records. Fix: pre-migration permission audit using the file migration tool's sibling Compare Permissions tool. - Duplicate uploads — running the migration twice without idempotency creates duplicate
ContentDocuments in target. Fix: use external IDs (or sourceContentDocument.Id) to detect already-migrated files on retry. - Lost metadata —
CreatedDateandOwnerIddefault to the migration user instead of the original. Fix: enable "Set Audit Fields upon Record Creation" in the target org and explicitly set audit fields during migration.
How to verify a successful migration
A clean migration produces the same answer to four questions in source and target (within the migrated scope):
- Count — how many
ContentDocuments andAttachments are linked to records of type X? - Total size — what's the sum of
ContentSizefor files linked to type X? - Per-record sample — pick 20 records, do they have the same files in source and target?
- Sharing — for those 20 records, do the same users have access to the files?
If all four match, the migration is verified. If counts match but sharing diverges, there's a permission gap. If counts match but sample records show file mismatches, there's an ordering or external ID problem.
Where tooling helps
The 8-step playbook above is the right approach regardless of how you execute it. The execution itself is where tools earn their keep — handling concurrency, retry, monitoring, and reporting so that the steps are reliable instead of aspirational.