Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

The quick version: pick the sandbox type by what the data is for, use a sandbox template so the copy is a coherent slice rather than a random sample, mask anything personal before a human logs in, and load in parent-before-child order with automation switched off. A developer sandbox with no data is not a testing environment, it is a compiler.

Choose the tier by the question you are answering

  • Developer. Metadata only, small storage, refreshes daily. Right for writing code, useless for anything about data shape or volume.
  • Developer Pro. Same, with more storage.
  • Partial Copy. Metadata plus a sample of production records, chosen by a sandbox template, capped per object. Refreshes on a multi-day cycle. This is the one most teams should be using for UAT and they mostly are not.
  • Full. Everything, at production volume, with the longest refresh interval. The only place a performance test or a field type conversion on a large object tells you the truth.

Sandbox templates and why the sample is not random

A template is a list of objects you want copied. Partial Copy requires one. Full accepts one if you want less than everything. Salesforce copies up to a per-object cap of records from each selected object, and it follows required relationships so that the copied records are usable rather than orphaned.

That last part is the thing to get right. Select Opportunity without Account and you get nothing coherent. Select Account, Contact, Opportunity, Case and the relevant custom objects together, and you get a slice that hangs together. Check the template against the object list your test scripts actually touch, not against a guess.

Masking, before anyone logs in

Copied production data is production data with the same obligations attached. Salesforce Data Mask is a managed package you install in production and run against the sandbox after refresh. It offers three treatments per field:

  • Anonymise: replace with random characters. Unreadable, and breaks anything that parses the value.
  • Pseudonymise: replace with realistic fake data of the same shape. Names look like names, emails look like emails. This is what you want for most fields.
  • Delete: blank the field entirely. For anything you should not hold at all.

Run it as part of the refresh procedure, not as a task someone remembers. Between the refresh completing and the mask finishing, the sandbox contains real customer data with sandbox-grade access control, so restrict logins until it is done.

Synthetic data

Options, roughly in order of effort: the CLI’s tree commands (sf data export tree and sf data import tree) for a small relational set with references resolved automatically. A recipe-driven generator such as Snowfakery for larger volumes with realistic distributions. Or a plain Apex script when you need the org’s own automation to produce the records.

sf data export tree --query "SELECT Id, Name, (SELECT LastName, Email FROM Contacts) FROM Account LIMIT 20" \
  --prefix seed --output-dir data/
sf data import tree --plan data/seed-plan.json --target-org mydev

The order of loading

Parents before children, and configuration before both.

  1. Metadata and configuration: record types, picklist values, custom settings, custom metadata.
  2. Users. Owners have to exist before the records that reference them. Match them by username or an external ID, not by name.
  3. Accounts, including the parent hierarchy loaded in passes if accounts reference each other.
  4. Contacts, then anything that points at both an account and a contact.
  5. Opportunities, Cases, custom transactional objects.
  6. Junction objects, once both sides exist.
  7. Activities, files and attachments, last, because they attach to everything.

Before any of it, deactivate the automation you do not need. Validation rules written for humans reject historical data routinely. Flows that send email will send it. Triggers that call out will call out, and a sandbox pointed at a production endpoint is a genuinely bad afternoon.

Give every object an external ID column from the source file and upsert on it. That makes the whole seed re-runnable, lets you load children by parent key without an ID lookup pass, and turns a failed load into something you fix and repeat rather than something you clean up.

Frequently asked

Does a sandbox refresh keep my seeded data? No. A refresh replaces the sandbox from production. Everything you loaded is gone, which is why the seed has to be a script.

Why are all my users deactivated? Sandbox copies deactivate most users and append a suffix to usernames. Reactivating the ones you need is a standard post-refresh step. Automate it with an Apex script.

How much data do I actually need? Enough to make the slow things slow. If a view loads instantly with fifty records and times out with fifty thousand, seeding fifty records tests nothing you care about.