function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion

How delete existing Apex Scheduled Jobs from Apex?

I can schedule a job via Apex code:


System.schedule('test', '0 0 0 * * ?', new SchedulableClass());


The CronTrigger job doesn't have a "Name" field, so I can't query for the Job I just created.  This means I can't check to see if my job already exists calling System.schedule(); instead I just have to call "schedule()" and silently eat the exception it throws if the job already exists.


The only way you can figure out which CronTrigger is yours is to cache the return value of System.schedule(), which (it so happens) is the ID of the CronTrigger that is created.  However, you can't delete them from Apex:



Id jobid = System.schedule('test', '0 0 0 * * ?', new SchedulableClass());
delete new CronTrigger(Id = jobid);

// 'delete' throws 'DML not allowed on CronTrigger'



So the current state of Scheduled Jobs is:


You can create them from Apex Code, but not from the UI

You can delete them from the UI, but not from Apex Code


I guess that just seems odd to me.  Why did Salesforce create this whole new API (System.schedule()), with a seemingly random assortment of ways to manipulate it, instead of just exposing the CronTrigger table directly to the full range of DML operations?


Placing new functionality into new core objects, rather than new APIs, seems easier on everyone (the whole describe/global describe suite of API calls are an example of something that seems a natural fit for a set of read-only custom objects).

Best Answer chosen by Admin (Salesforce Developers) 

Id not name:



String SCHEDULE_NAME = 'test'; id cronid = System.schedule(SCHEDULE_NAME, '0 15 0-23 * * ?', new scheduledMaintenance()); System.abortJob(cronid);


And to schedule via the UI:  Navigate to the Apex class list page - select "schedule apex"


let me know if this helps.



All Answers


Actually, you can't catch the exception thrown by System.schedule(), so there's no good way to idempotently create a scheduled task.


Thank you for the feedback.


1) You can create Scheduled Jobs from both the UI and Apex

2) You can delete Jobs from the UI or through Apex via System.abort(scheduled job id)

3) We should allow you to catch the exceptions - this looks like an oversight on our end.  You can however, query the Crontrigger to programmatically determine the available slots for scheduled jobs.

4) Not exposing the name field is another oversight.  I'll log a bug for this.  The workaround is to manage the jobs via the ID.


Taggart -


Thanks for the info!


I might not have been clear - I'm looking to un-schedule a job that might not currently be running (ie, a cron job that runs repeatedly via the CronTrigger table).  It's my understanding that System.abort() is for a jobs that are currently running (which may have been launched by a CronTrigger).


For example, the following code does not work:



string SCHEDULE_NAME = 'test';
System.schedule(SCHEDULE_NAME, '0 15 0-23 * * ?', new ScheduledMaintenance());



I get this exception on line 3:


System.StringException: Only batch and scheduled jobs are supported.


Also - maybe I'm missing something - but I don't see how to create a Scheduled Job in the UI.


My "Scheduled Jobs" page does not have a "New" button:



The entry in there was placed via Apex Code.  The "Del" link above is the only way I know of to delete it.


Id not name:



String SCHEDULE_NAME = 'test'; id cronid = System.schedule(SCHEDULE_NAME, '0 15 0-23 * * ?', new scheduledMaintenance()); System.abortJob(cronid);


And to schedule via the UI:  Navigate to the Apex class list page - select "schedule apex"


let me know if this helps.



This was selected as the best answer

Gotcha.  You might want to file a doc bug, then, as the docs state that abortJob() takes "String Job_name" rather than ID.  The docs also say that System.schedule() returns a string, but the value of that string (the job ID) is never mentioned in the docs either (I just lucked into it).





On a related note, creating Apex Sharing Recalculations is still in pilot.  As currently implemented in DE orgs, there's no way to specify the "scope" (batch size).  It would be great if we could specify the scope and package that info too, especially for those of us that need to use Dynamic Apex to insert our Share objects, which can only be done with single-row-inserts per [ this other post ].


I created a doc bug a few weeks back.  I will follow up with my doc writer to see what the status is.


Regarding the sharing recalc - why aren't you using the new batch interface?  Perhaps I'm missing something obvious.




We are using the batch interface ... I'm talking about when I associate a Batchable class with an custom object for automatic sharing re-calc when a customer changes their org-wide sharing model.  That UI  (for the association of the Batchable class with the custom object, on the custom object page) does not have a place to enter the batch size parameter.


Once I've created that association via the UI, salesforce will automatically call "executeBatch(associatedClass)" for me, but with the default batch size, which might be too large for my purposes (eg, my batchable class might only work if invoked with a batch size of 50 or some such).  Would be a non-issue except for the row-at-a-time SObject insert thing.



(thx for the doc update note - I'm working off a PDF from a month ago)


Sigh.  Do you have a case# with this request.  Seems a bit silly - we really should fix this.


The row-at-a-time limit on SObject DML is case 03474669.


I haven't opened a case about the recalc batch size UI.


It's now over 18 months later and query by Name is still not available.  is there any status on this?


Also would like an update on Query by name.


As you all know, "name" is still not exposed in the CronTrigger table.


The only way to handle this is to store, permanently, the CronTrigger ID as returned by System.schedule:



MyConfigObj__c.CronJobId__c = System.schedule(...);
update MyConfigObj__c;

So you can then delete it by ID.


If you, like us, have a bunch of these in the wild without having stored their ID (b/c it wasn't clear, at first, that System.schedule() returned the CronTrigger ID), then you (like us) will want to delete your old job & reschedule it so you can store the Job ID.


There are two things that can help you do this, even without a CronTrigger.Name field:


1.  If you try to schedule a job w/ the same name, you will get an exception


2.  Your CronExpression may be somewhat unique.


We actually have update code that leverages the above two facts to ensure we delete the old job (if it still exists) and then re-create it and store the Job ID that we get back.


The basic logic is this:  Loop through all jobs whose CronExpression matches yours, delete the candidate job & try to reschedule your job.  If you are able to reschedule, it means you deleted the right one.  If not, you deleted the wrong one and need to rollback your changes.


void rescheduleMyJob() {
  // corner case: it's possible they've already deleted the job by hand b/c they are crazy
  // if so, all we need to do is schedule it
  try {
    return; // succeeded = we're done here
  catch (Exception ok) { // prev job still exists in CronTrigger - must loop

  Savepoint nothingDone = null;
  for (CronTrigger t : [select Id, CronExpression from CronTrigger where CronExpression = :myCronExpression order by CreatedDate asc]) {
    try {
       // checkpoint within the loop so we can log in the exception handler
      nothingDone = Database.setSavepoint();


      // scheduleMyJob throws Exception if my job still exists in CronTrigger
      return; // job is rescheduled! get out!
    catch (Exception e) {
      // can't schedule the job = aborted the wrong job = rollback!
void scheduleMyJob() {
  // only works if job "myName" not already scheduled
  MyConfigObj__c.CronJobId__c = System.schedule(myName, myCronExpression, ...);
  update MyConfigObj__c;




There is a way to do this though it is undocumented and could be subject to breaking with new releases.

It is possible to add text after the year element of the Cron Expression - this text is ignored when the cron expression is parsed however it is available when querying the CronTrigger by CronExpression with a filter like "where CronExpression LIKE '% NAMEOFMYSCHEDULEDJOB'"

Interesting! Of course, if you're at the point where you're scheduling a job, you might as well just save its ID somewhere =)
So with some of the latest APIs, Salesforce has made the CronJobDetail object available, so this works now:
Id detailId = [SELECT Id FROM CronJobDetail WHERE Name='Your Job Name Here'][0].Id;
if (detailId != null) {
	Id jobId = [SELECT Id from CronTrigger WHERE CronJobDetailId = :detailId][0].Id;

Hitesh NarulaHitesh Narula
// loop through jobs located by name that we need to abort
for(CronTrigger ct : [SELECT Id, CronJobDetail.Name, CronJobDetail.JobType
                        FROM CronTrigger
                       WHERE CronJobDetail.Name like 'Work Order%']){
    // abort the job, try/catch because the job might not exist
    // if it's being aborted manually or from another execution
    } catch (exception e) {}
Phil WPhil W

Worth noting that System.abortJob can only be used in the context of a User with the "Modify All Data" system permission, which means you can't have this cancellation via abort for ordinary users. Also note that you can't query the CronTrigger instances for a given name and then delete them either. So the following will always fail with a DML operation Delete not allowed error:

List<CronTrigger> crons = [SELECT Id FROM CronTrigger WHERE CronJobDetail.Name = 'test'];
delete crons;
It seems really odd to me that an admin system permission is required to cancel jobs, even when they were submitted by the current User.
Randy LutcavichRandy Lutcavich
The trick is to temporarily delete the Schedulable job class that you used to start the job. This will delete all jobs, including the ones stuck in Queued.

More info on what caused this bug here:

Essentially jobs are getting scheduled in the past. Don't use a try/catch. Write an if check that adds a minute to the time if it is in the past.
Christopher D. EmersonChristopher D. Emerson
Thank you @KGalant. Your snippet was what I needed!