SfSkills Domains Docs GitHub

Home / devops / sandbox-data-isolation-gotchas

sandbox-data-isolation-gotchas

Use this skill when a sandbox has reached out to production systems, sent emails to real users, run live scheduled jobs, or leaked sensitive data after a refresh.

Open the skill folder on GitHub


Sandbox Data Isolation Gotchas

This skill activates when a sandbox has made unexpected contact with production systems — sending email to real users, firing scheduled jobs against live APIs, or leaking PII — after creation or refresh. It focuses on the isolation controls that are most commonly missed: email deliverability settings, scheduled job cleanup, integration endpoint resets, and the specific SandboxPostCopy behaviors that catch practitioners off-guard. It does not cover which sandbox type to choose, how to mask PII in copied records, or general refresh cycle management.


Before Starting

Gather this context before working in this domain:


Core Concepts

Email Deliverability — What Is and Is Not Controlled Automatically

Salesforce sandbox email deliverability is a two-layer control:

Layer 1 — Deliverability setting (Setup > Deliverability)

Every sandbox has an email deliverability setting with three levels:

New sandboxes created via the Setup UI default to System Email Only. However, sandboxes that were refreshed in older releases or promoted from an existing sandbox may inherit the previous deliverability setting — including All Email. Always verify the deliverability setting after every refresh.

Layer 2 — User email address obfuscation

When a sandbox is refreshed, Salesforce appends .invalid to all User object Email field values (e.g., jane.doe@example.com becomes jane.doe@example.com.invalid). This prevents password reset emails and system notifications from reaching real users.

What is NOT obfuscated: Contact, Lead, Person Account, and any other object's email fields are copied verbatim from production. If a workflow email alert targets a Contact's email, and deliverability is All Email, that real email address will receive the message. This is one of the most common sandbox data leakage vectors.

SandboxPostCopy Interface and Its Execution Constraints

The SandboxPostCopy Apex interface is the only platform-native hook for automating sandbox isolation immediately after refresh. However, it runs under constraints that practitioners frequently underestimate:

Scheduled Job Carry-Over

CronTrigger records (scheduled Apex jobs) are copied from production when a Full or Partial Copy sandbox is refreshed. Unlike User emails, scheduled jobs are not automatically deactivated. A scheduled Apex job that calls out to a production API, processes records, or sends email will execute on the same schedule as production from the moment the sandbox refresh completes.

The correct mitigation is to abort all CronTrigger records in the SandboxPostCopy class:

for (CronTrigger ct : [SELECT Id FROM CronTrigger WHERE State IN ('WAITING', 'ACQUIRED', 'EXECUTING')]) {
    System.abortJob(ct.Id);
}

Aborting CronTriggers requires the Automated Process user to have the "Manage Users" or relevant system permission. If the permission is missing, the abort will fail silently or throw — pre-grant via a permission set.

Integration Endpoint Carry-Over

Named Credentials are copied from production to the sandbox. Their endpoint URLs are preserved (pointing at production endpoints), but their secrets (passwords, OAuth tokens) are blanked. This creates two risks:

  1. If the Named Credential uses certificate-based or IP-allowlist-only auth, the sandbox may still successfully authenticate to the production endpoint using the same certificate that was copied.
  2. If the endpoint URL is not environment-specific (e.g., uses a production hostname), any callout from the sandbox — even one that appears to fail due to a missing secret — may reach the production system with partial or unauthenticated requests.

The SandboxPostCopy class should update Named Credential endpoint URLs to point at sandbox-appropriate test endpoints. However, Named Credential secrets (passwords, tokens) cannot be set programmatically — they require manual re-entry in Setup after refresh.


Common Patterns

Pattern 1: Pre-Flight Isolation Checklist in SandboxPostCopy

When to use: Every Full or Partial Copy sandbox refresh where the sandbox will be used for development, testing, or demos. Developer sandboxes copy no data so most of these concerns do not apply, but email deliverability still must be verified.

How it works:

global class SandboxIsolation implements SandboxPostCopy {
    global void runApexClass(SandboxContext context) {
        // 1. Abort all scheduled jobs
        List<CronTrigger> jobs = [
            SELECT Id FROM CronTrigger
            WHERE State IN ('WAITING', 'ACQUIRED', 'EXECUTING')
        ];
        for (CronTrigger ct : jobs) {
            System.abortJob(ct.Id);
        }

        // 2. Reset integration endpoints in Custom Metadata (Named Cred URLs
        //    must be updated manually; update any Custom Settings that store URLs)
        Integration_Config__c cfg = Integration_Config__c.getOrgDefaults();
        if (cfg != null) {
            cfg.API_Endpoint__c = 'https://api.sandbox.example.com';
            upsert cfg;
        }

        // 3. Log isolation actions
        System.debug('SandboxIsolation: completed for sandbox '
            + context.sandboxName()
            + ' orgId=' + context.organizationId());
    }
}

Why not the alternative: Relying on manual post-refresh runbooks means any missed step goes undetected until an incident occurs. SandboxPostCopy runs automatically and is the only supported hook that fires before users can log in and trigger workflows.

Pattern 2: Scrubbing Contact and Lead Emails Post-Refresh

When to use: Any Full or Partial Copy sandbox that will be used for QA, UAT, or demo scenarios where workflow email alerts, case auto-responses, or Apex emails might fire toward Contact/Lead email addresses.

How it works: Because Contact and Lead emails are not automatically obfuscated, the SandboxPostCopy class (or a Queueable chained from it) must update those fields. Do this in batches due to governor limits:

public class ObfuscateContactEmailsQueueable implements Queueable {
    public void execute(QueueableContext ctx) {
        List<Contact> contacts = [
            SELECT Id, Email FROM Contact
            WHERE Email != null
            AND Email NOT LIKE '%.invalid'
            LIMIT 10000
        ];
        for (Contact c : contacts) {
            c.Email = c.Email + '.invalid';
        }
        if (!contacts.isEmpty()) {
            update contacts;
        }
        // If more records remain, re-enqueue
        Integer remaining = [SELECT COUNT() FROM Contact
            WHERE Email != null AND Email NOT LIKE '%.invalid'];
        if (remaining > 0) {
            System.enqueueJob(new ObfuscateContactEmailsQueueable());
        }
    }
}

Chain this from SandboxPostCopy:

System.enqueueJob(new ObfuscateContactEmailsQueueable());

Why not the alternative: Setting deliverability to No Access would suppress the immediate risk, but it also suppresses legitimate system emails needed for QA testing. Explicit email scrubbing ensures the sandbox can run with System Email Only or All Email for testing purposes without risking leakage.


Decision Guidance

SituationRecommended ApproachReason
Sandbox emails reached real customers after refreshSet deliverability to System Email Only; scrub Contact/Lead emails via Queueable; investigate which workflow or Apex triggered the sendsEmail deliverability defaults to All Email in some refresh scenarios; Contact emails are not auto-obfuscated
Scheduled job fired against production API from sandboxAbort all CronTrigger records in SandboxPostCopy; update endpoint references in Custom SettingsScheduled jobs are copied in WAITING state and fire on schedule unless explicitly aborted
Named Credential still points at production endpointUpdate Named Credential URL manually in sandbox Setup; for non-secret URLs, set replacement value via Custom Settings in SandboxPostCopyNamed Credential secrets and URLs are copied; URLs cannot be changed programmatically pre-Spring '25
SandboxPostCopy class failed with permissions errorAssign a permission set to the Automated Process user covering the objects the class touches; re-run logic manuallyAutomated Process user has minimal default permissions; DML on custom objects fails without explicit grants
SandboxPostCopy ran but Contact emails were not changedContact/Lead email obfuscation is not built in; implement a Queueable from SandboxPostCopy to scrub those fieldsPlatform only obfuscates User emails; all other email fields are verbatim copies from production
Developer sandbox — concerned about data leakageVerify deliverability setting (System Email Only); no data is copied so Contact/Lead email risk does not applyDeveloper sandboxes contain metadata only, not production records

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Triage the incident or risk — determine whether this is a post-incident investigation (email was already sent, job already fired) or a pre-refresh checklist review. The output differs: incidents require root-cause analysis; pre-refresh requires a checklist walkthrough.
  2. Check email deliverability — in sandbox Setup > Deliverability, confirm the setting is System Email Only (or No Access if email testing is not required). Change it if needed; this takes effect immediately.
  3. Audit Contact and Lead email fields — run SELECT COUNT() FROM Contact WHERE Email != null AND Email NOT LIKE '%.invalid' in the sandbox. If the count is non-zero and deliverability is not No Access, scrub those records using the Queueable pattern above.
  4. Audit active scheduled jobs — run SELECT Id, CronJobDetail.Name, State FROM CronTrigger WHERE State != 'DELETED' in the sandbox. For each WAITING job, evaluate whether it connects to an external system or sends email. Abort those that do.
  5. Audit Named Credentials and integration config — list all Named Credentials in the sandbox (Tooling API or Setup UI). For each one, confirm the endpoint URL is a sandbox/test URL. Update Custom Settings or Custom Metadata records that store endpoint URLs.
  6. Verify or implement SandboxPostCopy — confirm a SandboxPostCopy class is registered on the sandbox record in production. If not, implement one using the isolation pattern above before the next refresh.
  7. Document gaps as runbook items — anything that cannot be automated (Named Credential secrets, OAuth token re-authorization) belongs in a written post-refresh runbook. Every manual step is a risk vector if skipped.

Review Checklist

Run through these before marking sandbox isolation work complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. Contact and Lead emails are NOT changed to .invalid on refresh — Only User object Email fields are obfuscated during sandbox creation and refresh. Contact, Lead, Person Account, and any other object's email-type fields are copied verbatim from production. Any workflow email alert, Apex Messaging.sendEmail(), or case auto-response that targets those fields will send to real addresses if deliverability is not No Access.
  2. SandboxPostCopy runs as Automated Process user with minimal permissions — The Automated Process user cannot access custom objects by default, cannot perform DML on many standard objects, and cannot call System.abortJob() without the appropriate system permission. If the SandboxPostCopy class silently fails, the first sign is often an isolation incident after users log in.
  3. Scheduled jobs fire immediately after refresh without warning — CronTrigger records are copied from production with State = 'WAITING'. They begin evaluating against their next fire time the moment the sandbox is available. A job that fires at 2:00 AM daily in production will fire at 2:00 AM in the sandbox the same night the refresh completes.
  4. Named Credential endpoint URLs survive the refresh unchanged — The secrets (passwords, OAuth tokens) are blanked, but the URL field is copied as-is. In orgs using certificate-based mutual TLS or IP-allowlisted OAuth, the sandbox can successfully call the production API even without an explicit secret. Always confirm Named Credential endpoint URLs in the sandbox point to test systems.
  5. Deliverability setting is not reset to a safe default on every refresh — Sandboxes created from scratch default to System Email Only. However, sandboxes refreshed from an existing sandbox that had All Email enabled can carry forward that setting. Do not assume the deliverability setting is safe after a refresh — always verify it explicitly.
  6. SandboxPostCopy exceptions do not surface in the sandbox Setup UI by default — If the class fails, the sandbox status page may show a generic completion message rather than an error. Enable debug logging for the Automated Process user before the refresh to capture the execution trace.

Output Artifacts

ArtifactDescription
Sandbox isolation pre-flight checklistOrdered checklist for email deliverability, Contact/Lead email scrubbing, scheduled job audit, and Named Credential review
SandboxPostCopy isolation classGlobal Apex class implementing SandboxPostCopy that aborts scheduled jobs and resets integration config on every refresh
Contact/Lead email scrub QueueableApex Queueable that appends .invalid to Contact and Lead email fields, chained from SandboxPostCopy
Integration endpoint audit queriesSOQL/Tooling API queries to identify Named Credentials and Custom Settings pointing at production URLs

Dependencies

Package files