Scheduled actions in Salesforce with Apex

Scheduled actions in Apex are great to use when you need to have a section of code run at a particular time in the future and Time-Based workflows will not work.  In the example below I’ll talk about how to schedule code to run at the first of every month, in addition talk about some constructs you can use to make your life easier when you have to redeploy/change this code

Schedulable

The key part of the apex class is that it must implement Schedulable and it must have an excecute method.

global class scheduledMonthly implements Schedulable {
    /**
    * Builds up all of the new Objects
    *
    * @param sc The schedulable context
    */
    global void execute(SchedulableContext sc) {
        //Code goes here
    }
}

Now, we can fill out the class.  Below is an example where we iterate through all of the accounts of a specific record type and insert a new list of MyObject based on that account.  This could be as complex as you want.

global class scheduledMonthly implements Schedulable {
    /**
    * Builds up all of the new Objects
    *
    * @param sc The schedulable context
    */
    global void execute(SchedulableContext sc) {
        RecordType rt = [
            select Id
            from RecordType
            where DeveloperName = 'Recipient'
        ];

        List<MyObject__c> objectList = new List<MyObject__c>();

        //Get all of the accounts of type 'Recipient'
        for (Account account: [
            select Id
            from Account
            where RecordTypeId = :rt.Id
        ]) {
            objectList.add(new MyObject__c(
                Account__c = account.Id
            ));
        }

        if (!objectList.isEmpty()) {
            insert objectList;
        }
    }
}

To schedule this class we could call it from any number of places, such as a VisualForce page, a Trigger or the Developer Console.  Since scheduled classes cannot be pushed out or changed when there are jobs in the queue we want to add a helper method to schedule this job.  In our instance it will only be ran once a month, so we include our CRON_EXP as a static variable (for easy use) and to reduce the change of mis-scheduling.

/**
* To schedule the monthly reconciliation:
*    NOTE: It should run at midnight on the first of every month on it's own, but if you make
*    changes and need to requeue run the command below from the developer's console
*
*    scheduledMonthly.scheduleIt();
*/

global class scheduledMonthly implements Schedulable {
    public static String CRON_EXP = '0 0 0 1 * ? *';

    /**
    * Static method used to schedule the default reconciliation
    *
    * @return The jobId from the scheduled run
    */
    global static String scheduleIt() {
        scheduledMonthly sm = new scheduledMonthly();
        return System.schedule('Monthly Reconciliation', CRON_EXP, sm);
    }

    /**
    * Builds up all of the new Monthly Reconciliations and Distributions
    *
    * @param sc The schedulable context
    */
    global void execute(SchedulableContext sc) {
        RecordType rt = [
            select Id
            from RecordType
            where DeveloperName = 'Recipient'
        ];

        List<MyObject__c> objectList = new List<MyObject__c>();

        //Get all of the accounts of type 'Recipient'
        for (Account account: [
            select Id
            from Account
            where RecordTypeId = :rt.Id
        ]) {
            objectList.add(new MyObject__c(
                Account__c = account.Id
            ));
        }

        if (!objectList.isEmpty()) {
            insert objectList;
        }
    }
}

To reset our schedule all we have to do is use type the following into the Developer console.

scheduledMonthly.scheduleIt();

And by looking in Setup->Monitoring->Scheduled Jobs we can see that our scheduledMonthly class is there.

Cron Syntax

There are seven fields for Salesforce’s cron syntax, unlike *nix’s 5 fields.

  • Seconds [0-59]
  • Minutes [0-59]
  • Hours [0-23]
  • Day of month [1-31]
  • Month [1-12 or JAN-DEC]
  • Day of week [1-7 or SUN-SAT]
  • Year [1970-2099]

There are also some special characters you can use:

  • , used to delimit values [hours, day of month, month, day of week, year]
  • – used to specify a range [hours, day of month, month, day of week, year]
  • * used to specify all values [hours, day of month, month, day of week, year]
  • ? used to specify no specific value [day of month, day of week]
  • / used to specify increments [hours, day of month, month, day of week, year]
  • L used to specify the end of a range [day of month, day of week]
  • W used to specify the nearest weekday [day of month]
  • # used to specify the nth day of the month [day of week]

Testing

Testing for scheduled apex may seem confusing but it’s very straight forward.  Like @future calls, scheduled apex will not fire until after the Test.stopTest()has been run.  In the test below we test the following:

  • The Cron Expression is the same
  • The job has not been triggered yet
  • The scheduled date is correct
  • No MyObjects were created
  • One object was created after the scheduled job was ran
@isTest
class scheduledMonthlyTest {
    public static RecordType fetchRecordType(String name) {
        return [
            select Id
            from RecordType
            where DeveloperName = :name
        ];
    }

    public static Account getAccount(RecordType rt) {
        return getAccount(rt, '_unittest_account_: 001');
    }

    public static Account getAccount(RecordType rt, String name) {
        return new Account(
            Name = name,
            RecordTypeId = rt.Id
        );
    }

    public static CronTrigger fetchCronTrigger(String jobId) {
        return [
            select CronExpression,
                TimesTriggered,
                NextFireTime
            from CronTrigger
            where Id = :jobId
        ];
    }
    public static Map<Id, List<MyObject__c>> fetchMyObjects(List<Account> accts) {
        Map<Id, List<MyObject__c>> result = new Map<Id, List<MyObject__c>>();

        for (Account a: accts) {
            result.put(a.Id, new List<MyObject__c>());
        }

        for (MyObject__c mo: [
            select Account__c
            from MyObject__c
            where Account__c in :result.keySet()
        ]) {
            result.get(mo.Account__c).add(mo);
        }
    }

    static testMethod void testScheduledMonthly() {
        RecordType rt = fetchRecordType('Recipient');

        Account testAccount = getAccount(rt);
        insert testAccount;

        Test.startTest();

        String jobId = System.schedule(
            '_unittest_scheduled_: 001',
            scheduledMonthly.CRON_EXP,
            new scheduledMonthly()
        );

        CronTrigger ct = fetchCronTrigger(jobId);

        System.assertEquals(
            scheduledMonthly.CRON_EXP,
            ct.CronExpression,
            'Did not get the same Cron Expression back'
        );
        System.assertEquals(
            0,
            ct.TimesTriggered,
            'The job has been run and should not have'
        );

        DateTime today = DateTime.now();
        String dateString = '' +
            today.year() + '-' + 
            today.addMonths(1).month() + 
            '-01 00:00:00';
        System.assertEquals(
            String.valueOf(DateTime.valueOf(dateString)),
            String.valueOf(ct.NextFireTime),
            'Did not get the right fire date'
        );

        List<MyObject__c> myObjs = fetchMyObjects(new List<Account>{testAccount}).get(testAccount.Id);
        System.assert(
            myObjs.isEmpty(),
            'Should have gotten no objects back'
        );

        Test.stopTest();

        myObjs = fetchMyObj(new List<Account>{testAccount}).get(testAccount.Id);
        System.assert(
            1,
            myObjs.size(),
            'Did not get the right number of objects back'
        );
    }
}

Limitations / Notes

There are three big limitations / notes about scheduled Apex

  • It will be put on the queue at the given time, it is not guaranteed to run at that time.
  • You can only have 25 classes scheduled at a time
  • You cannot use getContent or getContentAsPDFPageReference
This entry was posted in Development, Salesforce and tagged , , , , , . Bookmark the permalink.
  • Scott

    Can’t see when this was posted, so sorry if this is a necro, but this just helped me immensely with a project at work, and I just wanted to leave a note saying thanks. Really brilliant.

  • Glad to hear. The post done in may of 2012, but I still use it.

  • Regina

    Thank you so much for posting, this is very helpful! 🙂

  • sagar

    its very helpful. i got limitation for it.

  • raju

    How to schedule my batch class based on frequency
    Dynamically?
    Hi Sir,
    I have one frequency picklist field.I this picklist field
    values are Weekly,Monthly by day,Monthly by date and custom.When i
    select Weekly picklist value page rendered as
    Sunday,Monday…….Saturday checkboxes will be displayed.When i select
    Monthly by day picklist value page rendered as two picklists one is
    1st,2nd,3rd,4th and last ,second picklist as
    Sunday,Monday…….Saturday will be displayed.When i select Monthly by
    date picklist value page rendered as picklist as
    1st,2nd,3rd,4th……..31st and last will be displayed.When i select
    Custom value in picklist page rendered as
    DayOfMonth(Textbox),Month:(Textbox),DayOfWeek:(Textbox),Year:(Textbox)
    .I gave Job name ,Startdate,Enddate and PreferredStartTime field as
    Hours(12AM,1AM….11PM),Minuts(00,01,02….59) and
    seconds(00,01,02….59).

    NOTE:UI I designed how to perform backend operation based on my UI.

    How to add Frequency as CRON Expresson Dynamically From visualforce page?

    please help me…………..

  • To do this is pretty simple (in theory). What you’ll do is in the controller for that schedule page you create simply call the System.schedule call like described in the post. If this is something you plan on doing several times, you may want to look at something like Relax [1] which gives a better human interface for scheduling jobs.

    [1] https://github.com/zachelrath/Relax

  • mfphilbrick

    Patrick … great post. I have been trying to find an overview of scheduling apex jobs for hours. Yours seems to be the only start-to-finish explanation. Well done.

  • Pingback: Deleting all scheduled jobs in Salesforce with CasperJs » Deadlypenguin()

  • Wilayatai Faqih

    I have a trigger after some data validations , i am throwing an email to user.
    Our client wants this email to be send delaying half hour of this automation.

    Kindly suggest me some workaround.

    Note : I could not use time dependent workflow as email informations are being queried from different objects.

    Thanks

  • What I would do is to create a custom object that contains a target date and enough information to build your email. Then have a scheduled job that runs every 30min (or whatever frequency you want) and gets any unsent emails whose target date has already passed. Then build the email and send it.

    Alternately, you could use time dependent workflow to create a task on the object and then have a trigger on the task object that sends your email.

  • Wilayatai Faqih

    Thanks Patrick, I will look into these solutions. First one seems to be ideal. Once again thanks.