Daily cron jobs with Apex
To date, there has been no way to run code that manipulates Salesforce.com on a predetermined schedule, without having an external server and some sort of scheduler. Lots of people are running these kinds of scripts, but the folks I work with don’t have those kinds of resources. One of the things I love about on-demand is that I don’t have to keep servers running, so I shied away from writing these kinds of scripts.
In playing with Apex, I came up with a way to run Apex scripts every day, with no user intervention. It relies on two things: time-based workflow and Apex triggers. Here’s a run down of how it works.
I created a custom object called Batch Script and then created on instance of that object.

It’s got two fields that matter: a last run date field and a run checkbox. The next step is to create a timebased workflow on the object.

The workflow kicks off when the last run date is one day ago. When this state is reached, the run checkbox is updated.
Now, and Apex trigger fires on the update of the Batch Script object. Here’s the code:
trigger TestScript on Batch_Script__c bulk (after update) {
//if checkbox is checked, run the script
for (Batch_Script__c bs : Trigger.new) {
if (bs.Test_Script_Run__c == true) {
//run whatever APEX code you want
Batch_Script__c ourBS = new Batch_Script__c (
Id = bs.Id,
Test_Script_Run__c=false,
Test_Script_Last_Run_Date__c=system.today()
);
update ourBS;
}
}
}
This trigger runs arbitrary Apex code if the run checkbox is checked. After running the code, the run checkbox is unchecked and the last run date is set to today. Tomorrow, this will happen all over again–the code will be run and the object will be returned to a state where it will fire again in 24 hours.
Do you have more control with a cron job running on an external server? Sure. Is this a kludge? Absolutely. But if all you want to do is run daily jobs that use Apex to work with your data, you can use this method and save the headache of outside servers.
These new pieces of functionality are making things possible that weren’t possible before. That’s a great sign for the future of the platform–as long as they make more things possible, the platform will continue to get more powerful.

April 17th, 2007 at 7:37 pm
Brilliant !
April 17th, 2007 at 8:00 pm
Thanks Ron!
April 17th, 2007 at 10:46 pm
[...] Steve over at gokubi.com has been doing a lot with Apex Code recently. He has a clever post about writing Apex code triggers and having them run on a schedule. It’s a good post to get the wheels turning with regards to what you can now do on the platform. [...]
April 18th, 2007 at 12:30 pm
[...] Steve’s blog [...]
April 18th, 2007 at 12:48 pm
[...] Steve’s blog [...]
April 18th, 2007 at 1:24 pm
Very nice. I’m looking to call external web services using a similar approach. Anyone done this with outbound messages?
April 19th, 2007 at 9:34 am
While it is indeed an ingenious solution, I must let you aware of some of the governor limits for time-based workflow.
Because Time-based Workflow runs in a multi-tenant environment, the Time-based Workflow runtime engine strictly enforces the limits on the number of time triggers which can per processed by an organization to ensure that the shared resources are not monopolized. These limits, or governors, track and enforce the statistics outlined in the table below. If an organization ever exceeds a limit, the time-based workflow governor will stop processing and will defer all processing to the next window.
Edition Limit Window
Unlimited Edition 100,000 time triggers per org Day
Enterprise Edition 20,000 time triggers per org Day
So, when you are using this feature, please keep in mind the governor limits when processing large volumes of data.
April 19th, 2007 at 10:04 am
Thanks for the info Raja. My thoughts on using this technique is to run just a handful of scripts each day, so it’s nice to see the limits that high.
April 20th, 2007 at 7:23 am
Steve,
This is a great way to create a SF cron job. I envision this as a once or twice a day script that does something to my Salesforce objects, or to my external databases which sync with SF. Super cool. I don’t plan on hitting the 100,000 limit. Thanks again for being on the cutting edge.
Marc
April 20th, 2007 at 8:02 am
With this method, I can run a job once a day. I don’t think there is any way to run a job on a schedule other than daily increments because of the way time-based workflow works.
Maybe Salesforce.com will make this all easier by putting in a simple scheduling interface for Apex blocks. It would add to the app dev capabilities of the platform, that’s for sure.
April 22nd, 2007 at 7:09 am
A kludge to run more than 1x/day would be to change “Test Script Last Run Date” from a Date to a DateTime, then create multiple “Batch Script” objects with staggered times. In my testing, time-based workflows will execute 24 hours after the specific DateTime, although there is typically a 5-10 minute lag between the “Evaluation Date” in the workflow queue (when the workflow should, theoretically, fire) and when it actually executes.
For example: if you want a batch job to fire every 2 hours, you could create 12 Batch Script objects, each with a Test Script Run Date 2 hours later than the last. The practical limit would be around 10-15 minute intervals (due to the lag described above).
April 22nd, 2007 at 12:12 pm
Thanks for the tip Glenn. I haven’t played with execution times of time-based workflow and was a bit curious. Nice to hear from someone who’s looked into it a bit.
April 23rd, 2007 at 10:35 am
Fantastic!
April 25th, 2007 at 9:46 am
Hello Folks,
Wanted to give you an update on the limits. Please disregard the earlier numbers.
The limits are as follows:
• Unlimited Edition=20,000 time triggers per org
• Enterprise Edition=10,000 time triggers per org
For the latest numbers, please refer to the FAQ, which i will be posting soon on Time-based workflow feature detail page on successforce.com website.
May 4th, 2007 at 4:50 am
Very nice ! Like with many issues, which can only be solved with work arounds or external resources at the moment, APEX will do the trick. Very nice solution to run batches. Our company has developped quite a lot of batchtasks for customers, all hosted on external servers. I think your APEX solution will make running batches even easier in the future.
June 6th, 2007 at 1:49 pm
Love it – great work, Steve!
January 15th, 2008 at 4:22 pm
Nice work! I’ve taken the concept a bit further to allow time based hourly and daily scripts. http://scott.morrisonlive.ca/sfSimpleBlog/show/stripped_title/time-based-batch-scripts-in-salesforce
January 31st, 2008 at 10:10 am
I’m curious, does this approach still work? I’ve been playing with the same type of approach and have run into a roadblock (making me think Salesforce may have made changes to the way they process triggers and workflow):
The general flow goes like this (I’ve simplified the debug output for brevity sake) …
BatchObject is modified, RunBatchJob=true.
Beginning triggerX on BatchObject (After Update)
Batch job runs and sets RunBatchJob=false
Beginning triggerX on BatchObject (After Update)
This time RunBatchJob=false, so no action taken.
Ending triggerX
Beginning Workflow Evaluation
RunBatchJob=false so criteria evaluates to true and time-based action is queued.
Ending workflow evaluation
Ending triggerX
Beginning Workflow Evaluation
RunBatchJob=true (!! apparently the original version of BatchObject) so criteria evaluates to false and time-based action is removed from queue
Ending workflow evaluation
How do I stop the Workflow Evaluation at the end from occuring and undoing my work? I’ve tried setting separate flags for “run apex” and “run scheduler”, and I’ve run the trigger before and after update, but before the trigger/workflow cycle can complete, my trigger has to update a field used in the workflow rule and this problem re-appears.
May 19th, 2008 at 8:08 am
Ron,
I hit the same problem and adjusted my code to create a new BatchObject (which I call Scheduled_Job__c) record every time it gets set off, instead of updating the same record. It gives me a log of what’s happened, and I then have a method, also run by the batch routine, that deletes old Scheduled_Job__c records to keep things clean.
This approach also works for me because I wrote my routine to process records in chunks that I know won’t set off the governor limits (always a battle), so the code generates a new Scheduled_Job__c to run immediately until all records are processed and then schedules itself again for the next night at midnight. You just need some way to track which records have been processed already, which I do by just adding a timestamp field to the records that I’m processing that gets updated when the batch is run. I’ve used it to run some typical batch processes and also to send out a dynamic email each night to a bunch of users, where I’m limited to sending to 10 email addresses at one time and about 40 people need to receive it.
It’s a hack on a hack on a hack, but it works! Luckily, I’m nowhere near hitting the limit on workflow time triggers per day. This is the trigger I’m using-
trigger InsertUpdateScheduledJobAfter on Scheduled_Job__c (after insert, after update) {
Integer intReturn;
Integer intMagicNum;
for (Scheduled_Job__c sj : Trigger.new){
if (sj.Run_Job__c == true){
//Create Site Log records for 7 business days in the future, if needed
if (sj.Method_To_Execute__c == ‘CreateSiteLogs’){
intReturn = clsScheduledJobs.CreateSiteLogs(null);
//This job is run in batches of 2 site invoices (the max that can be processed
//before hitting the governor limit), so less than 2 means it’s done
intMagicNum = 2;
}
//Send out Job Loss Report
if (sj.Method_To_Execute__c == ‘JobLossReport’){
intReturn = clsScheduledJobs.JobLossReport();
//Single email limited to 10 at a time
intMagicNum = 10;
}
if (sj.Method_To_Execute__c == ‘ScheduledJobCleanup’){
intReturn = clsScheduledJobs.ScheduledJobCleanup();
//This job always returns 0, shouldn’t need to be re-run anytime soon
intMagicNum = 1;
}
Scheduled_Job__c new_sj = new Scheduled_Job__c(
Method_To_Execute__c = sj.Method_To_Execute__c,
Name = sj.Name,
Run_Job__c = false,
Schedule__c = true);
//If the number returned by the method is less than the Magic Number,
//create a Scheduled Job record to run it again tomorrow night
if (intReturn < intMagicNum){
//Reset Scheduled Job record to midnight of tomorrow
new_sj.Next_Run_Datetime__c = Datetime.newInstance(System.today().year(), System.today().month(), System.today().day()).addDays(1);
//Otherwise, there are more records to process, so create a record
//to run it again immediately
} else {
new_sj.Next_Run_Datetime__c = System.now();
}
insert new_sj;
}
}
}
July 23rd, 2008 at 6:13 am
[...] One of the things salesforce.com lacks is a decent analog to “cron”, the ubiquitous unix/linux job scheduling tool. This is a kludgey but mildly amusing solution. (Here’s a simple, less kludgey cron that’s all native SFDC, which is probably a better i… [...]
July 25th, 2008 at 6:30 pm
[...] One of the things salesforce.com lacks is a decent analog to “cron”, the ubiquitous unix/linux job scheduling tool. This is a kludgey but mildly amusing solution. (Here’s a simple, less kludgey cron that’s all native SFDC, which is probably a better i… [...]
August 4th, 2008 at 1:55 pm
[...] of you who follow Steve Andersen’s blog may have seen his post re: cron jobs using Apex in April 2007. Kudos to you, Steve for a fantastic post (better late than never!) not only because [...]
April 10th, 2009 at 8:43 pm
Ron, Jessie, Steve
Have the same problem. I guess updating the same record again doesn’t help anymore. I tried inserting new records and it works, but not the update. I guess this is what is preventing:
When a record is created, or when a record is edited and did not previously meet the rule criteria
Correct me if i am wrong. I tried it and is not firing the rule the second time, after it is updated in the trigger. Wondering how to fool the rule to think that it didn’t meet the criteria previously. Guess thats not possible!
September 23rd, 2009 at 11:57 pm
has SFDC put any scheduling interface for Apex blocks? pls let me know
November 25th, 2009 at 5:29 am
This is fantastic – thank you.