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.
Since I can’t rely on email, I am going to use a combination of a scheduled trigger and an external notification path.
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.
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.
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.
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};
}
}
}
Create an Autolaunched Flow. Here is the logic you should build:
Here’s what it might look like:
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());
}
}
}
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!
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 * * * ?';
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.

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.
Our website uses cookies for analytics, styling, and functional purposes. By accepting cookies, you'll get the best version of our website - and we'll be able to process some of your data to make that happen.