Sandbox Post-Refresh Automation
When a sandbox is refreshed (full, partial, dev, dev-pro), the
metadata + data are copied from the source. Users are then locked
out for a final preparation step before the sandbox becomes
available. The SandboxPostCopy interface is Salesforce's hook
into that step: an Apex class you specify in the sandbox refresh
configuration runs ONCE, before users can log in.
It's the right place to:
- Mask emails so test sends don't fire to real customers.
- Deactivate users who shouldn't have sandbox access.
- Scrub integration endpoints that point at production systems.
- Disable scheduled jobs that would race with prod (replicator, escalator, etc.).
- Repopulate test data that doesn't survive the copy (or shouldn't — auth tokens, caches, etc.).
- Reapply environment-specific config (named credentials, Custom Setting values keyed to environment, feature flags).
Without it, every refresh is a manual cleanup checklist that inevitably gets forgotten on one of the steps. The most common forgotten-step disaster: prod-pointing integration endpoints stay configured; a developer in the sandbox triggers the integration; production data changes.
What this skill is NOT. The strategic question of how many
sandboxes / which tier / refresh cadence lives in
devops/sandbox-strategy-designer. Generic Apex scheduling is
apex/scheduled-apex. This skill is specifically about the
post-refresh hook and what it should do.
Before Starting
- Confirm the sandbox tier matters. All tiers support SandboxPostCopy. Some org features (e.g. partial-data sandbox data sampling) interact with the post-copy step.
- Decide which actions are non-negotiable. Email masking is always non-negotiable. Endpoint scrub is non-negotiable for orgs with prod-pointing integrations. Scheduled-job disable is non-negotiable if any scheduled job mutates external state.
- Document the runbook explicitly. The post-copy class IS the runbook; it's executable documentation. Future you reads it.
- Plan idempotency. The class runs once per refresh; failure during the run can leave the sandbox half-prepared. Build the class to be safe to re-run if needed.
Core Concepts
The SandboxPostCopy interface
global class MySandboxPrep implements SandboxPostCopy {
global void runApexClass(SandboxContext context) {
// Runs once, after sandbox refresh, before users log in.
maskEmails();
deactivateProdOnlyUsers();
scrubIntegrationEndpoints();
disableScheduledJobs();
repopulateSampleData();
applyEnvironmentConfig();
}
}
Three things to know:
globalis required. Internalpublicwon't satisfy the interface contract.SandboxContextexposessandboxName(),sandboxId(), andorganizationId(). Use these for environment-aware logic ("if dev sandbox, do X; if full sandbox, do Y").- Configured at refresh time. Setup → Sandboxes → click the sandbox → Apex Class field. Specify the class name. Saved with the sandbox; persists across refreshes.
What runs vs what doesn't get copied
The metadata + data copy doesn't include:
- Login Hours / Login IP Ranges on profiles (security intentional).
- Outbound message workflow targets (always reset to a "do not deliver" placeholder by Salesforce — but verify in your org).
- Scheduled jobs — they DO copy, which is usually the wrong outcome. Sandbox post-copy disables them.
- Single-sign-on certificates for some IdP integrations.
What DOES copy and is dangerous in sandbox:
- Named Credentials — point at prod by default unless the named credential was already environment-aware.
- Connected App configurations — OAuth callback URLs are prod-shaped.
- Custom Setting values — usually carry prod values; rewrite in post-copy.
- Workflow + Apex outbound emails — if email deliverability is "All emails", the sandbox can fire emails to real customers unless masked.
Email masking patterns
Hardest constraint. A refreshed sandbox with no email mask + deliverability = "All emails" + a developer running a test = real customers receive sandbox-generated emails.
Mitigation A: Org-wide deliverability "System emails only". Setup → Email → Deliverability → Access Level → System emails only. Done at the org level; survives across refreshes if set on the sandbox post-copy. Belt-and-suspenders with email masking.
Mitigation B: Mask user emails.
private static void maskEmails() {
List<User> toUpdate = new List<User>();
for (User u : [SELECT Id, Email FROM User WHERE Email != NULL AND IsActive = TRUE]) {
// Replace @ with .invalid + a tag — emails won't deliver, but
// can be reverse-engineered for debugging.
u.Email = u.Email.replace('@', '+sandbox@') + '.invalid';
toUpdate.add(u);
}
update toUpdate;
}
The .invalid TLD is reserved by IETF for non-routable use —
mail servers reject delivery. The +sandbox@ tag preserves the
original local-part for forensic debugging.
Disabling scheduled jobs
private static void disableScheduledJobs() {
for (CronTrigger ct : [
SELECT Id FROM CronTrigger
WHERE State IN ('WAITING', 'ACQUIRED', 'EXECUTING')
]) {
try {
System.abortJob(ct.Id);
} catch (Exception ex) {
// Some jobs can't be aborted (in-flight); log and continue.
}
}
}
Aborts every scheduled / queued job. A safer variant only aborts jobs whose name matches a documented list of "production-only" jobs; over-aggressive abort can break sandbox tests.
Scrubbing integration endpoints
Named Credentials, Custom Settings, Custom Metadata Types — all copy from production with prod values. Post-copy, rewrite to sandbox / mock endpoints:
private static void scrubIntegrationEndpoints() {
// Named Credentials are not directly Apex-mutable for endpoint URL;
// typically scrubbed via metadata deploy in the sandbox setup script.
// Custom Setting / Custom Metadata are mutable.
Integration_Config__c cfg = Integration_Config__c.getOrgDefaults();
cfg.Endpoint__c = 'https://mock.example-sandbox.com/api';
cfg.API_Key__c = 'SANDBOX-MOCK-KEY';
update cfg;
}
For Named Credentials, the sandbox-prep team usually pre-deploys sandbox-pointing Named Credential metadata as part of the post-copy + a metadata deploy step.
Decision Guidance
| Situation | Approach | Reason |
|---|---|---|
| New sandbox tier added | Implement SandboxPostCopy from scratch |
Standard pattern |
| Existing sandbox with manual cleanup checklist | Codify the checklist in post-copy | Manual is forgotten one step at a time |
| Multi-environment Custom Setting values | Custom Setting + post-copy assignment | Centralized; deterministic |
| Prod-pointing Named Credentials | Pre-deploy sandbox NC metadata + post-copy callout (or just rely on deploy) | NC URL not Apex-mutable directly |
| Email deliverability concerns | System emails only + email masking (both) | Belt-and-suspenders |
| Scheduled jobs that mutate external state | System.abortJob in post-copy |
Critical safety mechanism |
| Sandbox-specific feature flags | Custom Metadata + post-copy update | Survives across refreshes |
| Different post-copy for dev vs full sandbox | Use SandboxContext.sandboxName() to branch |
Environment-aware behavior |
| Failed post-copy run mid-execution | Make every step idempotent; document re-run procedure | Failures happen; design for them |
Recommended Workflow
- Inventory the manual post-refresh checklist. Email masking, user deactivation, endpoint scrub, scheduled-job disable, sample data, env config.
- Build the
SandboxPostCopyclass — one private method per checklist item. - Make every method idempotent. Re-running shouldn't break anything.
- Configure on every sandbox. Setup → Sandboxes → Apex Class field.
- Test by refreshing a low-tier sandbox first. Verify each method ran (audit log, masked emails, disabled jobs).
- Document the class in source control. It's runbook-as-code.
- Keep
Sandboxorg-wide deliverability at "System emails only" as a belt-and-suspenders default.
Review Checklist
- Class implements
SandboxPostCopywithglobalaccess. -
runApexClasscovers email masking, user deactivation, endpoint scrub, scheduled-job disable, sample data, env config. - Every step is idempotent (safe to re-run).
-
SandboxContextis consulted when behavior should differ per environment. - Class name is configured in Setup → Sandboxes for every sandbox.
- Org-wide deliverability is "System emails only" as a default.
- Failed-run procedure is documented (how to re-run).
Salesforce-Specific Gotchas
SandboxPostCopyrequiresglobalaccess modifier.publicdoesn't satisfy the interface contract. (Seereferences/gotchas.md§ 1.)runApexClassruns ONCE per refresh. Mid-execution failure leaves the sandbox half-prepared; design for re-runnability. (Seereferences/gotchas.md§ 2.)- Scheduled jobs DO copy from prod to sandbox; they fire in the sandbox unless explicitly aborted in post-copy. (See
references/gotchas.md§ 3.) - Named Credential URLs are not Apex-mutable. Pre-deploy sandbox-pointing NC metadata or accept a stale URL. (See
references/gotchas.md§ 4.) - Email deliverability resets to "System emails only" on Salesforce's side after refresh — but verify; not all orgs / tiers behave identically. (See
references/gotchas.md§ 5.) SandboxContext.sandboxName()is the developer-given name, not a hash; rely on it cautiously for branching. (Seereferences/gotchas.md§ 6.)- The post-copy class must be already deployed in production before refresh; the sandbox copies the class FROM production. (See
references/gotchas.md§ 7.)
Output Artifacts
| Artifact | Description |
|---|---|
SandboxPostCopy Apex class |
The post-copy automation, one method per concern |
| Test class | Asserts each method's effects (mask, deactivate, scrub) |
| Sandbox configuration documentation | Which class is configured on which sandbox tier |
| Manual override runbook | How to re-run the class if it fails mid-execution |
Related Skills
devops/sandbox-strategy-designer— strategic decision (how many sandboxes, what tiers, refresh cadence). This skill is the implementation half.apex/scheduled-apex— generic scheduled / batch Apex; sandbox-post-copy is a specific runtime.admin/email-templates-and-alerts— email infrastructure; post-copy interacts with deliverability config.apex/apex-mocking-and-stubs— for the test class that exercises the post-copy methods.