The Production is Silent Prevention Plan: Monitoring Salesforce Email Deliverability

In this article...

Ping Ping: “Hey, so I have noticed that the automated welcome emails aren’t being sent out.”

We’ve all had that heart-stopping moment. You check everything. Then, you check the Salesforce Email Deliverability settings. Someone (maybe even you) accidentally toggled it to “System Email Only” in Production instead of the Sandbox, an easy mistake to make! But half a day has passed, and a ton of automated emails have not been sent. Oh, the headache of such a simple mistake.

Simply put, if Deliverability is off, Salesforce can’t send an email to tell you it’s off. It’s the ultimate Catch-22. Thankfully, this is a Catch-22 I am up for resolving.

My name is Jeremy Hutchinson, Director at Desynit Limited, a Salesforce Partner based in Bristol. Today, I am going to build a “Dead Man’s Switch” using Apex and Flow to ensure you’re notified via Slack, Teams, or Custom Notifications the second your email status changes.

 

The Salesforce Architecture: A Fail-Safe Loop

Since I can’t rely on email, I am going to use a combination of a scheduled trigger and an external notification path.

1. The Probe: DeliverabilityChecker (Salesforce Apex)

Salesforce doesn’t give us a DeliverabilityChangedEvent. Instead, I have to “test the pipes.” I use an Invocable Method that leverages Messaging.reserveSingleEmail(count). This method doesn’t actually send an email; it just asks the system, “If I wanted to send 1 email, would you let me?” If it throws an exception, I know deliverability is restricted.

2. The Brain: Salesforce Autolaunched Flow

This is where the Admin magic happens. The Flow calls our Apex probe, compares the result against a Custom Setting (holding the Last Known State), and decides if an alert is needed.

3. The Clock: ScheduledFlowRunner (Salesforce Apex)

Standard Scheduled Flows are great, but they can only run once per day; sometimes, I want more granular control or the ability to trigger a specific Autolaunched Flow logic via the Scheduler. This class acts as the heartbeat, firing off our logic at a frequency of your choosing.

Step 1: The Salesforce Apex Probe

You’ll need this simple Invocable Method to bridge the gap between Flow and the messaging engine

public class DeliverabilityChecker {
    @InvocableMethod(label='Check Email Deliverability' description='Returns true if All Email is enabled in the org. Returns false if All Email is not enabled or if there is no capacity to send an email.')
    public static List checkAllEmailEnabled() {
        try {
            // Attempt to reserve the capacity for a single email. This wil be released when the transaction ends. If All Email is not set or there is no capacity, an exception will be thrown.
            Messaging.reserveSingleEmailCapacity(1);
            return new List{true};
        } catch (System.NoAccessException e) {
            // No permission to send API or mass email, usually because deliverability is not All Email
            return new List{false};
        } catch (System.HandledException e) {
            // The daily limit for the org would be exceeded by this request. I'll treat this as if deliverability is not All Email
            return new List{false};
        }
    }
}

Step 2: The Logic – Salesforce Visual Flow

Create an Autolaunched Flow. Here is the logic you should build:

  1. Call Apex: Use the DeliverabilityChecker.
  2. Get State: Look up a Custom Setting (e.g., Email_Monitor_State__c) to see what the status was 15 minutes ago.
  3. Decision: * If State hasn’t changed: End.
    • If the State has changed: Update the Custom Setting and sound the alarm.
  4. The Alarm: Since Email is unreliable here, use an Action to:
    • Post to a Slack/MS Teams channel via a Webhook.
    • Send a Custom Notification to the Salesforce Mobile App.
    • Invoke an API to an external monitoring tool (like PagerDuty).

Here’s what it might look like:

Step 3: Scheduling with Apex

If you wanted to check every 10 minutes, you’d need 144 scheduled flows, each running once per day.  That’s way too many flows, so for this reason, I can use a small Apex class to run the Flow on a scheduled basis.  

Now, there are limits here too, as an Apex class can be scheduled once per hour, and a maximum of 100 concurrent scheduled Apex jobs. But assuming there is capacity, I can use Anonymous Apex to schedule the check every 15 minutes

// Pass the API Name of your specific flow here ScheduledFlowRunner
myJob = new ScheduledFlowRunner('<>');
// Schedule every 15 minutes
System.schedule('Deliverability check: 00', '0 0 * * * ?', myJob);
System.schedule('Deliverability check: 00', '0 15 * * * ?', myJob);
System.schedule('Deliverability check: 00', '0 30 * * * ?', myJob);
System.schedule('Deliverability check: 00', '0 45 * * * ?', myJob);

 

Now, to run the Anonymous Apex above, I’ll need the ScheduledFlowRunner Apex class below.  This takes the API name of an Autolaunched Flow and runs it on the schedule provided.

 

public class ScheduledFlowRunner implements Schedulable {
   
    private String flowApiName;


    // Constructor: Pass the exact API Name of the Autolaunched Flow
    public ScheduledFlowRunner(String flowApiName) {
        this.flowApiName = flowApiName;
    }
    public void execute(SchedulableContext sc) {
        try {
            if (String.isBlank(flowApiName)) {
                // If no Flow API name is provided, do nothing
                // System.debug('ScheduledFlowRunner Error: No Flow API Name provided.');
                return;
            }
            // 1. Instantiate the Flow dynamically using the provided name
            Map<String, Object> inputs = new Map<String, Object>();
            Flow.Interview myFlow = Flow.Interview.createInterview(flowApiName, inputs);

            // 2. Fire the Flow
            myFlow.start();

        } catch (System.TypeException te) {
            // Silently fail if the Flow doesn't exist or is inactive. Uncomment the next line to see this in the debug log.
            // System.debug('Error: Flow "' + flowApiName + '" not found or is inactive. ' + te.getMessage());
        } catch (Exception e) {
            // Silently fail if any other exception occurs. Uncomment the next line to see this in the debug log.
            // System.debug('General Exception in ScheduledFlowRunner: ' + e.getMessage());
        }
    }
}

Why This Rocks for Salesforce Sandboxes, Too

This isn’t just a Production safety net. How many times have you started a UAT (User Acceptance Testing) phase only to realise halfway through the day that no one got their test emails because the Sandbox was still on System Email Only?

By deploying this to your Sandboxes, you can have the system automatically ping your #Dev-Team Slack channel:

Sandbox Alert: Deliverability in UAT-Full has been changed to All Email. Proceed with caution!

And, speaking of Salesforce Production

Salesforce does not allow deployments of Apex Classes into Production without associated tests that provide sufficient code coverage.  So you’ll need to also create the following two Apex Test classes and include them in any promotion to Production

 @IsTest
public class DeliverabilityCheckerTest {

    @IsTest
    static void testCheckAllEmailEnabled() {
        // We cannot force the Org setting to change in a test,
        // so we test the current state of the environment.
       
        Test.startTest();
        List results = DeliverabilityChecker.checkAllEmailEnabled();
        Test.stopTest();
       
        // Validation
        Assert.areNotEqual(null, results, 'The result list should not be null');
        Assert.areEqual(1, results.size(), 'The result list should have exactly one entry');
       
        // Note: In most scratch orgs and CI environments, this will return True.
        // If it returns False, it confirms the 'catch' block in your class is working.
        System.debug('Deliverability Status in Test: ' + results[0]);
    }
}

@isTest
private class ScheduledFlowRunnerTest {

    @isTest
    static void testFlowScheduling() {
        // Use a dummy name; in a real test, you'd use a Flow that exists in your org
        String targetFlow = 'My_Autolaunched_Flow';
        String jobName = 'Test Hourly Flow Job';
        String cronExp = '0 0 * * * ?';

        Test.startTest();
       
        // 1. Schedule the class, passing the flow name into the constructor
        ScheduledFlowRunner runner = new ScheduledFlowRunner(targetFlow);
        String jobId = System.schedule(jobName, cronExp, runner);
       
        // 2. Verify the job is in the queue
        CronTrigger ct = [SELECT Id, CronExpression, State FROM CronTrigger WHERE Id = :jobId];
        System.assertEquals(cronExp, ct.CronExpression, 'The CRON expression should match.');
       
        // 3. This forces the 'execute' method to run
        Test.stopTest();
       
        // After stopTest(), the job has 'fired' in the test context.
        System.assertNotEquals(null, jobId, 'Job ID should be generated.');
    }
   
      @isTest
    static void testFlowSchedulingNoName() {
        // Use no name. Job will still be scheduled but we'll hit the "No Flow API name" debug message
        String targetFlow = '';
        String jobName = 'Test Hourly Flow Job No Name';
        String cronExp = '0 0 * * * ?';

 

The Sleep Better Result for Salesforce Email Deliverability

By implementing my Salesforce Email Deliverability solution above, I have moved from reactive (waiting for a frustrated user) to proactive (knowing within minutes of a configuration mishap).

I can’t stop humans from making mistakes, but I can certainly sleep better knowing that I have built a system that catches them before they become critical incidents. And now you can too.

____________________________________________________________________________

Jeremy Hutchinson: Director at Desynit Limited

If you like this article, why not check out Desynit Director, Jeremy Yearron’s post on Faster Apex Tests.

Work with Desynit

Looking for exceptional, professional Salesforce support?

Our independent tech team has been servicing enterprise clients for over 15 years from our HQ in Bristol, UK. Let’s see how we can work together and get the most out of your Salesforce implementation.