Deploying code in Salesforce is usually a straightforward process, but if you have ever worked with scheduled jobs, you have likely encountered the frustrating deployment block. You try to push a change to a class, only to be met with an error stating that the class has pending or in-progress scheduled jobs.
This happens because Salesforce, by default, locks any Apex class that is currently referenced by the CronTrigger system. This safeguard prevents the platform from running code that might be in a state of flux during a deployment. However, in a fast-paced development environment, waiting for a job to finish or manually unscheduling jobs is not always feasible.
In this guide, we will explore the different ways to bypass this limitation, from platform-level settings to advanced architectural patterns that decouple your logic from the scheduler.
Understanding the 'Class Locked' Error
When you schedule a class using the Schedulable interface, Salesforce creates a link between the job and the class. If you attempt to deploy a new version of that class via Change Sets, Salesforce CLI, or the Ant Migration Tool, the deployment will fail.
Historically, the only solution was to manually delete the scheduled job, perform the deployment, and then reschedule the job. For organizations with dozens of scheduled tasks, this manual intervention is a significant bottleneck and a major risk for human error.
Solution 1: The Salesforce Deployment Setting (Recommended)
Since the Winter '15 release, Salesforce has provided a built-in setting that allows you to deploy code even when jobs are active. This is the most direct way to solve the problem without changing your code architecture.
How to Enable the Setting
- Navigate to Setup in your Salesforce org.
- In the Quick Find box, type Deployment Settings.
- Locate the option: “Allow deployments of components when corresponding Apex jobs are pending or in progress.”
- Check the box and click Save.
Pros: - No code changes required. - Works with all deployment tools (Metadata API, SFDX, Change Sets). - Simplifies the CI/CD pipeline.
Cons: - There is a minor risk if the job starts exactly while the metadata is being swapped, though Salesforce generally handles this gracefully by finishing the job with the previous version of the code.
Solution 2: The Dynamic Dispatcher Pattern
Before the platform setting was introduced, legendary Salesforce expert Dan Appleman presented a pattern at Dreamforce 2013 designed to decouple the scheduler from the business logic. While some developers find this pattern less necessary today due to the platform setting, it remains a powerful architectural choice for complex integrations.
How the Pattern Works
The core idea is to create a "Dispatcher" class that implements Schedulable. This dispatcher does not contain any business logic. Instead, it uses the Type.forName method to dynamically instantiate a separate handler class at runtime.
Because the dispatcher class is the only one actually "scheduled," it is the only one that gets locked. The handler class, which contains your actual logic, remains unlocked and can be deployed at any time.
Code Example: The Scheduled Dispatcher
global class ScheduledDispatcher Implements Schedulable {
public Interface IScheduleDispatched
{
void execute(SchedulableContext sc);
}
global void execute(SchedulableContext sc)
{
// Dynamically resolve the handler class
Type targettype = Type.forName('ScheduleHandler');
if(targettype != null) {
IScheduleDispatched obj = (IScheduleDispatched)targettype.NewInstance();
obj.execute(sc);
}
}
}
public class ScheduleHandler implements ScheduledDispatcher.IScheduleDispatched {
public void execute(SchedulableContext sc)
{
// Your complex business logic goes here
System.debug('Executing scheduled logic...');
}
}
Why Use This Pattern?
By using an Interface (IScheduleDispatched), you ensure that your dispatcher can call any class that follows the contract. This decoupling is excellent for modularity and unit testing, as you can test the ScheduleHandler independently of the Salesforce scheduling engine.
Important Caveats and Metadata API Behavior
It is important to note that even with the "Deployment Setting" enabled, some developers have reported issues when using the Ant Migration Tool or specific package.xml configurations. If you include both the scheduled class and the job metadata in a single deployment package, the system may still trigger a validation error in certain edge cases.
Additionally, if you are developing a Managed Package, Salesforce handles this differently. Package upgrades generally do not require you to unschedule jobs, as the platform has internal mechanisms to manage versioning during the upgrade process.
Best Practices for Scheduled Apex
To avoid deployment headaches and maintain a healthy Salesforce org, follow these best practices:
- Keep Logic Out of the Schedulable Class: Use the Schedulable class only as a wrapper. Delegate all heavy lifting to a Service class or a Domain class.
- Use Custom Metadata for Configuration: If your scheduled job needs specific parameters (like batch sizes or query filters), store them in Custom Metadata. This allows you to change the job's behavior without deploying new code.
- Check for Active Jobs in Tests: When writing unit tests for scheduled jobs, always use
Test.startTest()andTest.stopTest()to ensure the asynchronous work completes before you verify your assertions. - Monitor the Scheduled Jobs Page: Regularly check the Scheduled Jobs page in Setup to ensure you don't have "zombie" jobs running that are no longer needed.
Frequently Asked Questions
Can I unschedule an Apex job programmatically?
Yes, you can use the System.abortJob(jobId) method. You can query the CronTrigger object to find the ID of the job you wish to cancel. This is often used in installation scripts or maintenance utilities.
Does the 'Allow Deployment' setting affect performance?
No, this setting does not impact the performance of your Apex code or the execution speed of your jobs. It simply changes the validation rules used by the Metadata API during a deployment.
Why was my class still locked even after enabling the setting?
This usually happens if you are trying to change the signature of the execute method or if there is a fundamental conflict in the metadata. In rare cases, clearing the browser cache or re-logging into the org can resolve synchronization issues with Setup settings.
Wrapping Up
Deploying scheduled Apex no longer has to be a manual, error-prone process. For most teams, enabling the Deployment Setting to allow updates during active jobs is the most efficient path forward. However, for those building highly modular systems, the Dynamic Dispatcher pattern offers a layer of abstraction that keeps your core logic decoupled and highly maintainable.
By implementing these strategies, you can ensure your CI/CD pipeline remains smooth and your scheduled tasks continue to run without interruption.