+ Start a Discussion
Radhe ShyamRadhe Shyam 

How to schedule apex class after every 5 minutes

Here is my apex batch:

global class EmailAlertToQuoteProposalApprovalUser implements Database.Batchable<sObject>, database.stateful{
    
    public String EVENT_TYPE_MEETING = 'Meeting';
    private Datetime EndTime=System.now();
    private String query;
    public String limitSize;
    private long recordcount;
    private string debuglog='';
    private integer batchcounter=0;
    private datetime processstarttime=system.now();
    private boolean haserror=false;
    private set<id> processedaccounts=new set<id>();
   
    global EmailAlertToQuoteProposalApprovalUser(datetime activitydatetime){
        if(activitydatetime==null){
            EndTime=System.now().adddays(-1);
        }
    }
   
   global Database.QueryLocator start(Database.BatchableContext BC){
       log('Batch process start time: ' + processstarttime);
       log('Batch process id: ' + bc.getJobID());
       log('Acvitity End Time Parameter: ' + EndTime);
       Datetime dtTimeNow=system.now();
       query='SELECT Id,Pending_With_Email__c,Submitted_Date_time__c FROM Apttus_Proposal__Proposal__c WHERE Submitted_Date_time__c!=null AND Submitted_Date_time__c>=: dtTimeNow' + ' ORDER BY Id';
       if(limitSize!=null)
       {
        query = query + ' LIMIT ' +  limitSize;
       }     
            
       log(query); 
       return Database.getQueryLocator(query);                                    
      
    }
   
    global void execute(Database.BatchableContext BC, List<sObject> scope){
        log('Batch number: ' + batchcounter);
        set<id> QuoteProposalId=new set<id>(); 
        list<Apttus_Proposal__Proposal__c>  lstQuoteProposal=new list<Apttus_Proposal__Proposal__c>();
        for(sobject so: scope){
            id quoteid=(Id)so.get('Id');
            Apttus_Proposal__Proposal__c objQuote= (Apttus_Proposal__Proposal__c)so;
            if(!processedaccounts.contains(quoteid)){
                QuoteProposalId.add(quoteid);
                lstQuoteProposal.add(objQuote);
            }
        }
        if(QuoteProposalId.size()>0){
            log('Number of accounts to be processed: ' + QuoteProposalId.size());
            log('Account Ids: '+ QuoteProposalId);
            List<String> CCAddress=new List<String>();
            CCAddress.add('abaranwal@kloudrac.com');
            string TemplateId=system.Label.quoteEmailTemplate;
            
            processedaccounts.addAll(QuoteProposalId);
            try{
                Messaging.SingleEmailMessage[] mails=new Messaging.SingleEmailMessage[0];
                 
                for(Apttus_Proposal__Proposal__c qt:lstQuoteProposal){
                    list<string> AdditionalRecipients=new list<string>();
                    AdditionalRecipients.add(qt.Pending_With_Email__c);
                    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                    mail.setToAddresses(AdditionalRecipients);
                    mail.setCcAddresses(CCAddress);
                    //mail.setOrgWideEmailAddressId(OrgWideEmailId);
                    mail.setTemplateId(TemplateId);
                    mail.whatid=qt.id;
                    mail.setTargetObjectId('003n0000008FULE'); 
                    mails.add(mail);

                }
                
                log('Sending emails ... count: ' + mails);
                Messaging.sendEmail(mails); 
            
                        
            }catch(Exception e) {
                haserror=true;
                string body=e.getMessage();
                log(body);
    
            }
        }else{
            log('No Quote/Proposal is processed in this batch');
        }
        ++batchcounter;
    }

    global void finish(Database.BatchableContext BC){
        log('Entire batch process has ended');
        SaveDebugLog();
    } 
    
    public static void SendEmail(String Subject,String Body,String[] Recipeints){
        /*String Displayname=Lookup.getTarget('UpdateOpportunity', 'Config','FromAddress', true);
        //OneCRM.sandbox@ge.com
        Id OrgWideEmailId=[Select Id, DisplayName, Address from OrgWideEmailAddress where Address =:Displayname].Id;    
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        mail.setToAddresses(Recipeints);
        mail.setSubject(Subject);
        mail.setPlainTextBody(Body);
        mail.setOrgWideEmailAddressId(OrgWideEmailId);
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail});*/
                
    }
    

    private void log(string msg){
        debuglog+=msg+'\n';
        system.debug(logginglevel.info,msg);
    }
    
    private Id SaveDebugLog(){
        String recStr =  'Transaction Log';
        
        Integration_Log__c log= new Integration_Log__c();
        log.Call_Method__c = 'BatchProcessQuoteProposalEscalationEmail';
        log.Object_Name__c = 'Quote/Proposal';
        log.Call_Time__c = processstarttime;            
        log.Status__c = haserror?'Failure':'Success';
        insert log;        
        
        recStr += '\nInterfaceId:' + log.Object_Name__c;
        recStr += '\nObjectId:' + log.Object_Id__c;
        recStr += '\nCallTime:' + log.Call_Time__c.format('yyyy.MM.dd  HH:mm:ss.SSS z');
        recStr += '\nStatus:' + log.Status__c;
        recStr += '\nLogId:'+ log.Id;
        recStr += '\n';            
        recStr += debuglog;
        
        Blob recBlob= Blob.valueOf(recStr);
        Attachment att= new attachment();
        att.Name = 'Log Details ' +system.now()+'.txt';
        att.ParentId = log.Id; 
        att.Body = Blob.valueof(recStr); 
        insert att;     
        
        return log.id;
    }    
    
    public static void startbatch(datetime activitytime){
        
        EmailAlertToQuoteProposalApprovalUser aula=new EmailAlertToQuoteProposalApprovalUser(activitytime);
        aula.log('activitytime: ' + activitytime);
        aula.EndTime=activitytime;
        if(activitytime==null){
            aula.EndTime=System.now().adddays(-1);
        }
        ID batchprocessid = Database.executeBatch(aula);
        System.debug('Apex Job id: ' + batchprocessid );
    }
}


And here is schedulable:  I want to run batch file after every 5 minutes... Please Help
===============================================

global class scheduledQuoteReminderBatchable implements Schedulable {
   global void execute(SchedulableContext sc) {
       EmailAlertToQuoteProposalApprovalUser aula=new EmailAlertToQuoteProposalApprovalUser(system.now());
       ID batchprocessid = Database.executeBatch(aula);
       
   }
}
Ketankumar PatelKetankumar Patel
By default you can run apex job every 1 hour using cron expression but you can schedule this job 12 times at 5 min duration. However only 100 Apex classes can be scheduled concurrently and 5 batch Apex jobs can be queued or active concurrently.

http://resources.docs.salesforce.com/198/10/en-us/sfdc/pdf/salesforce_app_limits_cheatsheet.pdf
 
String sch1 = '0 0 * * * ?';
scheduledQuoteReminderBatchable sqrb1 = new scheduledQuoteReminderBatchable();
system.schedule('Every Hour plus 0 min', sch1, sqrb1);

String sch2 = '0 5 * * * ?';
scheduledQuoteReminderBatchable sqrb2 = new scheduledQuoteReminderBatchable();
system.schedule('Every Hour plus 5 min', sch2, sqrb2);

String sch2 = '0 10 * * * ?';
scheduledQuoteReminderBatchable sqrb2 = new scheduledQuoteReminderBatchable();
system.schedule('Every Hour plus 10 min', sch1, sqrb2);

String sch3 = '0 15 * * * ?';
scheduledQuoteReminderBatchable sqrb3 = new scheduledQuoteReminderBatchable();
system.schedule('Every Hour plus 15 min', sch3, sqrb3);
.
.
.
.
//You get the idea.
.
.
.
.
String sch12 = '0 55 * * * ?';
scheduledQuoteReminderBatchable sqrb12 = new scheduledQuoteReminderBatchable();
system.schedule('Every Hour plus 55 min', sch12, sqrb12 );
James LoghryJames Loghry
Currently, you're limited to running a batch class every hour via the Salesforce UI.  However, you can use Apex or Execute Anonymous from the dev console / mavensmate to kick off a batch class in smaller intervals than that.  You do this by using a cron syntax, as shown in this SSE post: http://salesforce.stackexchange.com/questions/37333/how-to-run-a-scheduled-job-every-15-minutes

Beware though, as batch classes hardly ever kick off on time.  That is, they kick off whenever the resources are available after they are supposed to start.  I've gone down the "run batch apex every 5 minute" road before, and the batch jobs typically overlap pretty quickly.  In other words, I would consider spacing your batch jobs out to at least 15 minutes.
James LoghryJames Loghry
Also, if you want that minute of a timeframe, you might have to create multiple schedules of the same batch job.  For instance, one batch kicks off at 0:15 of the hour, the second is scheduled for 0:30, third is scheduled for 0:45, fourth is scheduled for 00:00.
Radhe ShyamRadhe Shyam
Thanks@Ketankumar Patel,
I am new in this apex batch and all,

please help me out to write the proper code: How to insert your code in this below code.....or we need to write some othe class file?

global class scheduledQuoteReminderBatchable implements Schedulable {
   global void execute(SchedulableContext sc) {
       EmailAlertToQuoteProposalApprovalUser aula=new EmailAlertToQuoteProposalApprovalUser(system.now());
       ID batchprocessid = Database.executeBatch(aula);
       
   }
}

 
Ketankumar PatelKetankumar Patel
Hi Radhe,

You don't need to insert my code into your code. If you have valid scheduable class then you need to run each block of below code in developer console. That will schedule your batch class.
String sch1 = '0 0 * * * ?';
scheduledQuoteReminderBatchable sqrb1 = new scheduledQuoteReminderBatchable();
system.schedule('Every Hour plus 0 min', sch1, sqrb1);
Radhe ShyamRadhe Shyam
But i need to write it once some where, the class should be get execute after every 5 minuts. is it possible?
Nitish TalekarNitish Talekar
To run Schedule a Class In Every 5 Mins in Salesforce,
Check this out: http://howtodoitinsalesforce.blogspot.in/2016/12/run-schedule-class-in-every-5-mins-in.html
Christina ZhankoChristina Zhanko
Hi, Radhe! I had a similar problem.
Try to use Schedule Helper. Allows you to manage your schedules to execute even every 5 minutes.
From AppExchange - https://appexchange.salesforce.com/listingDetail?listingId=a0N3A00000DqCmYUAV
docbilldocbill
It is 2018, and still the same problem.  We did the really stupid thing 6 years ago, which is I wrote one scheduled job that will invoke all our our batch jobs based on custom settings, and scheduled that custom job for every minute of every day.   It actually works well, but the problem is that left us just 40 job slots for managed packages.   Now we are about 20 short of what we need...

I was looking for something a bit more clever than the self rescheduling trick.   The problem with that is when salesforce gets busy, that can turn a once in every 1 minute schedule to a once every 5 minute schedule...  So far no simple solutions, just complex ones.

This wouldn't be such an issue if salesforce would implement the ability to schedule more than once an hour, or if they were to make managed packages use their own schedule queue, rather than sharing same queue with unmanaged applications.
 
Jatin NarulaJatin Narula
Hello Everyone,

You can use this logic to achieve it:

Integer frequency = 15;    // interval in minutes you want apex to schedule ie. 2, 3, 5, 10, 15
Integer totalIterations = 60/frequency - 1;

for(Integer i = 0; i <= totalIterations; i++){
    Integer count = i*frequency;
    String minuteString = (count < 10) ? '0' + count : '' + count;
    System.schedule('Scheduled Job ' + i,  '0 ' + minuteString + ' * * * ?', new ApexScheduler());  //  ApexScheduler is a class that implements schedulable interface 
}

Let me know if you have any questions.


Best,
Jatin Narula
 
Ashwin KhedekarAshwin Khedekar
You can make an Apex scheduler class to reschedule its n number of future runs through the code in the class which implements the Schedulable interface. Suppose you want to reschedule 20 runs at gap of 5 minutes each, then just change "count" in below code in the while loop :- while(count < 5) change to while(count < 20) :-

// The scheduler class :-
global without sharing class SchedulerClass2 implements Schedulable
{
    Integer count = 0;
    Integer gapMinutes = 5;
        
    public void execute(SchedulableContext sc) {
    
        System.debug(LoggingLevel.INFO, '#### execute method starts of SchedulerClass2');

        // Re-schedule only once if "Check x min trigger" field does not have
        // value "WorkerClass" in it. This is a custom field of type text on Account object.
        // API name of this field is :- 
        Account a = [Select id, name, Check_x_min_trigger__c from Account where name = 'ListAcc10000'];
        // Create an account with name = 'ListAcc10000'. Keep its field "Check x min trigger" empty.

        System.debug(LoggingLevel.INFO, '#### queried account is ' + a);
        System.debug(LoggingLevel.INFO, '#### queried account Check_x_min_trigger__c is ' + a.Check_x_min_trigger__c);
        
        // When the batch class code runs, it will populate the value "WorkerClass" in the field "Check x min trigger"
        // of all the accounts in the database
        if(a.Check_x_min_trigger__c != 'WorkerClass')
        {
            System.debug(LoggingLevel.INFO, '#### rescheduling SchedulerClass2');
            // Re-schedule ourself 5 times to run again at gap of 5 minutes
            while(count < 5) {
                DateTime now  = DateTime.now();
                DateTime nextRunTime = now.addMinutes(gapMinutes);
                System.debug(LoggingLevel.INFO, '#### nextRunTime is ' + nextRunTime);
                
                String cronString = '' + nextRunTime.second() + ' ' + nextRunTime.minute() + ' ' + 
                    nextRunTime.hour() + ' ' + nextRunTime.day() + ' ' + 
                    nextRunTime.month() + ' ? ' + nextRunTime.year();
                // format for cronString is seconds minutes hours day_of_month month day_of_week year
                System.debug(LoggingLevel.INFO, '#### calculated cronString is ' + cronString);
        
                System.schedule(SchedulerClass2.class.getName() + '-' + nextRunTime.format(), cronString, new SchedulerClass2());
                count++;
                System.debug(LoggingLevel.INFO, '#### count incremented, new count is ' + count);
                gapMinutes = gapMinutes + 5; // add gap of 5 minutes for each future run.
                System.debug(LoggingLevel.INFO, '#### gapMinutes incremented, new gapMinutes is ' + gapMinutes);
            }
        }
        
        // Launch a batch job to do the actual work
        WorkerClass wc = new WorkerClass();
        System.debug(LoggingLevel.INFO, '#### about to call executeBatch of WorkerClass');
        Database.executeBatch(wc);
    }
}

// The batch class which will do the actual work. It will populate the value "WorkerClass" in the custom field
// "Check x min trigger" on all account records in the database.
global class WorkerClass implements Database.Batchable<SObject>
{
    global Database.QueryLocator start(Database.BatchableContext bc)
    {
        String query = 'Select id, name, City__c, Check_x_min_trigger__c from Account';
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC, list<SObject> scope)
    {
        System.debug(LoggingLevel.INFO, '#### execute method of batch class starts in WorkerClass');
        List<Account> accLstToUpdate = new List<Account>();
        if(scope != null)
        {
            List<Account> accLst = (List<Account>)scope;
            for(Account a : accLst)
            {
                a.Check_x_min_trigger__c = 'WorkerClass';
                accLstToUpdate.add(a);
            }
            
            if(accLstToUpdate.size() > 0)
            {
                System.debug(LoggingLevel.INFO, '#### execute method of batch class: updating accLstToUpdate ' + accLstToUpdate.size());       
                update accLstToUpdate;
            }
        }
    }

    global void finish(Database.BatchableContext BC)
    {
        System.debug(LoggingLevel.INFO, '#### finish method of batch class');
    }
}

From Developer Console -> Execute Anonymous, call the scheduler class :-
SchedulerClass2 sc2 = new SchedulerClass2();
String stringTime = '0 55 * * * ?'; // will run at 55 minutes past every hour
String jobID = system.schedule('SchedulerClass2Scheduler', stringTime, sc2);

Go to Setup -> Jobs -> Scheduled Jobs to see the future run Apex Scheduler jobs queued up.
Coding-With-The-ForceCoding-With-The-Force
Hey there, I know this thread is a bit old, but I figured out a super good way to do this by having the scheduled class reschedule itself every 5 minutes. This way you only have one scheduled job that re-schedules itself every five minutes.

There's a video walkthrough here and there's links to my github code example in the video's description: https://youtu.be/NjY51eURQXc
amar sharma 7amar sharma 7
global class testScheduleClass  implements Schedulable {
 
    private final String JOBNAME = 'Repeating Job';
    private final Integer FIVEMINUTE =5;

    public void execute(SchedulableContext cont)
    {    System.debug('every 5 min');
        findAndAbortJob(cont);
    }

    private void findAndAbortJob(SchedulableContext cont)
    {
        if (cont == null)
        {
            return;
        }
        
        List<CronJobDetail> cronDetail = [SELECT Id FROM CronJobDetail WHERE Name= :JOBNAME LIMIT 1];

        if (cronDetail.isEmpty())
        {
            return;
        }

        
        List<CronTrigger> cronTriggers = [SELECT Id FROM CronTrigger WHERE CronJobDetailId = :cronDetail[0].Id];

        if(cronTriggers.isEmpty())
        {
            return;
        }

        try
        {
        
            System.abortJob(cronTriggers[0].Id);
            rescheduleJob();
        }
        catch (Exception e)
        {
            System.debug('This was the error ::: ' + e.getMessage());
        }
    }
    public  void rescheduleJob()
    {
        Datetime sysTime = System.now().addMinutes(FIVEMINUTE);
        String cronExpression = '' + sysTime.second() + ' ' + sysTime.minute() + ' ' + sysTime.hour() + ' ' + sysTime.day() + ' ' + sysTime.month() + ' ? ' + sysTime.year();
        System.schedule(JOBNAME, cronExpression, new testScheduleClass());
    }    
}

 
Rishabh Mathur 11Rishabh Mathur 11
Thank you Amar Sharma, this code works for me.
Suraj Tripathi 47Suraj Tripathi 47

Hi Radhe,
Cron Expression to run a Job every 5 Minutes is given below
    
System.schedule('Schedule Job Name 1',  '0 00 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 2',  '0 05 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 3',  '0 10 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 4',  '0 15 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 5',  '0 20 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 6',  '0 25 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 7',  '0 30 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 8',  '0 35 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 9',  '0 40 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 10', '0 45 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 11', '0 50 * * * ?', new ScheduleBatchApexClassExample());
System.schedule('Schedule Job Name 12', '0 55 * * * ?', new ScheduleBatchApexClassExample());



If you find your Solution then mark this as the best answer.

Thank you!
Regards,
Suraj Tripathi  
docbilldocbill

Here is the way I do this.  First I install https://github.com/docbill/Managed-Scheduled-Apex/projects  (https://github.com/docbill/Managed-Scheduled-Apex/projects) into my org, and use the quickstart command to schedule with the maximum frequency I need for my batch jobs.  At Red Hat we use once every two minutes, because experience has taught us a higher frequency just creates too much locking.

Then I implement my class extending AbstractBatchableBase or AbstractBatchable.   The main difference here between just using the Batchable interface, is I can implement a hasWork() method to return false, where there are no records to process, avoiding the batch call.  I usually will not hard code constants inside my batch job, but instead allow the constants to be assigned by a json constructor.

Here is a screen shot of scheduled jobs and what type of values we configure:

User-added image

 

Notice how we actually can run Cleanup_Batchable under many different input sets for cleaning up different types of records.  So it means less code overall.  Adding a new cleanup job is typically a 2 point story.  The effort is mainly testing to make sure we configured the job correctly.

While the other answers people are giving you are correct, they are only good for a small org with very few scheduled jobs running.  You'll quickly find all your available jobs slots are full if you try to schedule many jobs multiple times per hour.  Almost all our jobs slots at Red Hat are filled by managed packages, which don't give us the ability to use our managed scheduled apex class.   

However, this is also very dark path.  As ultimately scheduled jobs eventually are limited in how much they can be scaled.  And if you get too many of them, you'll find you start to become bottlenecked.

 

 

docbilldocbill

BTW. I decided to add the Cleanup_Batchable class to the open source.  As this job contains no internal business logic for Red Hat, and is a good example that many would actually use without need to modify it.

 

// Copyright Red Hat
// SPDX-License-Identifier: GPL-3.0-only
/**
 * This is a generic class for deleting obsolete records.  This is intended to be use with a json contructor that assigns the soql string.   With great power, comes great
 * responsability.  As something like { soql: 'select Id from Opportunity'; } would proceed to delete every single opportunity in the system!   As a saftey feature, this class
 * will only delete objects with a field named:  Cleanup_Eligible__c, and that field must return a true value.
 *  
 * In addition to the literals defined https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_dateformats.htm it there are some
 * other constant values defined.  These include: 
 * :LAST_PROCESSED, :NOW, :HOURS_AGO_1, :HOURS_AGO_2, ..., :HOURS_AGO_23
 * 
 * If the Cleanup_Eligible__c field contains all the selection criteria, a minimum json constructor would be:
 *    { 
 *       "lastProcessedKey: "APTS_SKU_Staging.LastProcessed", 
 *     }
 * 
 * An example that specifies the time range, and explicitly assigns the from object would be:
 *    { 
 *       "lastProcessedKey: "APTS_SKU_Staging.LastProcessed", 
 *       "fromObject" : "APTS_SKU_Staging__c",
 *       "criterias" : [ "SystemModStamp >= :LAST_PROCESSED", "systemModStamp < :LAST_N_MONTHS:3","Status__c != 'New'" ]
 *     }
 * 
 * For debugging, it might be usef specify a query instead:
 *    { 
 *       "lastProcessedKey": "APTS_SKU_Staging.LastProcessed", 
 *       "soql": "select Id from APTS_Sku_Staging__c where SystemModStamp >= :LAST_PROCESSED and SystemModStamp < LAST_N_MONTHS:3 and Status__c != 'New' and Cleanup_Eligible__c = true" 
 *     }
 * 
 * @version 2018-09-11
 * @author Bill C Riemers <briemers@redhat.com>
 * @since 2018-09-11 - Created
 */
global class Cleanup_Batchable extends AbstractBatchable {
	/**
	 * How often in hours should we run this even with no work?
	 */
	global static final Integer MIN_FREQUENCY = 3 * 24;
	/**
	 * The default elilible Field
	 */
	global static final String ELIGIBLE_FIELD = 'Cleanup_Eligible__c';

	/**
	 * Cleanup Eligible Field.
	 * 
	 */
	global String eligibleField {
		get {
			if(eligibleField == null) {
				eligibleField = ELIGIBLE_FIELD;
			}
			return eligibleField;
		}
		set;
	}

	/** 
	 * Name of a custom setting key for last processed datetime. Assigning this will also auto assign systemModStampMin.   Although not required, this value is strongly advinced.
	 * the normal naming convention would be <fromObject>.Cleanup.   If you have multiply jobs for the same object append something unique to this. 
	 */
	global String lastProcessedKey = null;

	/** Name of the object to query.  Normally this will be assigned from the leading part of the lastProcessedKey with __c appended.   */
	global String fromObject {
		get {
			if(String.isBlank(fromObject) && ! String.isBlank(lastProcessedKey)) {
				fromObject = lastProcessedKey.split('[.]',2)[0]+'__c';
			}
			return fromObject;
		}
		set;
	}

	/**
	 * How often in hours should we run this even with no work?
	 */
	global Integer minFrequency {
		get {
			if(minFrequency == null) {
				minFrequency = MIN_FREQUENCY;
			}
			return minFrequency;
		}
		set;
	}

	/** Dynamic query */
	global String soql {
		get {
			if(soql == null && ! String.isBlank(fromObject) ) {
				// always have an eligible field criteria unless the feature 
				// has been disabled by assigning a blank value to eligibleField
				List<String> criterias = this.criterias.clone();
				String eligibleField = this.eligibleField;
				if(! String.isBlank(eligibleField)) {
					for(String c : criterias) {
						if(c.trim().startsWithIgnoreCase(eligibleField)) {
							eligibleField = null;
							break;
						}
					}
					if(eligibleField != null) {
						criterias.add(eligibleField +' = true');
					}
				}
				soql = 'SELECT Id FROM '+fromObject+' WHERE '+String.join(criterias,' AND ');
			}
			return soql;
		}
		set;
	}

	/** Dynamic query conditions */
	global List<String> criterias {
		get {
			if(criterias == null ) {
				criterias = new List<String>();
			}
			return criterias;
		}
		set;
	}


	/** 
	 * Keep track of the lastProcessedDateTime
	 */
	global DateTimeSetting__c lastProcessed {
		get {
			if(lastProcessed == null) {
				if(lastProcessedKey != null) {
					lastProcessed = DateTimeSetting__c.getInstance(lastProcessedKey);
				}
				if(lastProcessed == null || lastProcessed.Value__c == null) {
					lastProcessed = new DateTimeSetting__c(Name=lastProcessedKey,Value__c=DateTime.newInstance(2000,1,1));
				}
			}
			return lastProcessed;
		}
		set;
	}

	/**
	 * Default constructor.
	 */
	global Cleanup_Batchable() {
		super(null);
	}

	/**
	 * Check if there is work for this job to do.
	 *
	 * @return false if there is no work to do.
	 */
	global override Boolean hasWork() {
		final DateTime NOW = DateTime.now();
		final DateTime HOURS_AGO_1 = NOW.addHours(-1);
		final DateTime HOURS_AGO_2 = NOW.addHours(-2);
		final DateTime HOURS_AGO_3 = NOW.addHours(-3);
		final DateTime HOURS_AGO_4 = NOW.addHours(-4);
		final DateTime HOURS_AGO_5 = NOW.addHours(-5);
		final DateTime HOURS_AGO_6 = NOW.addHours(-6);
		final DateTime HOURS_AGO_7 = NOW.addHours(-7);
		final DateTime HOURS_AGO_8 = NOW.addHours(-8);
		final DateTime HOURS_AGO_9 = NOW.addHours(-9);
		final DateTime HOURS_AGO_10 = NOW.addHours(-10);
		final DateTime HOURS_AGO_11 = NOW.addHours(-11);
		final DateTime HOURS_AGO_12 = NOW.addHours(-12);
		final DateTime HOURS_AGO_13 = NOW.addHours(-13);
		final DateTime HOURS_AGO_14 = NOW.addHours(-14);
		final DateTime HOURS_AGO_15 = NOW.addHours(-15);
		final DateTime HOURS_AGO_16 = NOW.addHours(-16);
		final DateTime HOURS_AGO_17 = NOW.addHours(-17);
		final DateTime HOURS_AGO_18 = NOW.addHours(-18);
		final DateTime HOURS_AGO_19 = NOW.addHours(-19);
		final DateTime HOURS_AGO_20 = NOW.addHours(-20);
		final DateTime HOURS_AGO_21 = NOW.addHours(-21);
		final DateTime HOURS_AGO_22 = NOW.addHours(-22);
		final DateTime HOURS_AGO_23 = NOW.addHours(-23);
		final DateTime LAST_PROCESSED = lastProcessed.Value__c;
		Boolean retval = ((! String.isBlank(lastProcessed.Name)) && NOW.addHours(-minFrequency) > lastProcessed.Value__c);
		if(! retval) {
			System.debug('Query: '+soql);
			for(SObject o : Database.query(soql+' limit 1')) {
				retval = true;
			}
		}
		return retval;
	}

	/**
	 * Start method impl for Database.Batchable interface.  A fairly small
	 * scope value will need to be used for the current implementation.
	 * 
	 * @param   bc batchable contents
	 * @return  list of ready records
	 */
	global Database.QueryLocator start(Database.BatchableContext bc) {
		final DateTime NOW = DateTime.now();
		final DateTime HOURS_AGO_1 = NOW.addHours(-1);
		final DateTime HOURS_AGO_2 = NOW.addHours(-2);
		final DateTime HOURS_AGO_3 = NOW.addHours(-3);
		final DateTime HOURS_AGO_4 = NOW.addHours(-4);
		final DateTime HOURS_AGO_5 = NOW.addHours(-5);
		final DateTime HOURS_AGO_6 = NOW.addHours(-6);
		final DateTime HOURS_AGO_7 = NOW.addHours(-7);
		final DateTime HOURS_AGO_8 = NOW.addHours(-8);
		final DateTime HOURS_AGO_9 = NOW.addHours(-9);
		final DateTime HOURS_AGO_10 = NOW.addHours(-10);
		final DateTime HOURS_AGO_11 = NOW.addHours(-11);
		final DateTime HOURS_AGO_12 = NOW.addHours(-12);
		final DateTime HOURS_AGO_13 = NOW.addHours(-13);
		final DateTime HOURS_AGO_14 = NOW.addHours(-14);
		final DateTime HOURS_AGO_15 = NOW.addHours(-15);
		final DateTime HOURS_AGO_16 = NOW.addHours(-16);
		final DateTime HOURS_AGO_17 = NOW.addHours(-17);
		final DateTime HOURS_AGO_18 = NOW.addHours(-18);
		final DateTime HOURS_AGO_19 = NOW.addHours(-19);
		final DateTime HOURS_AGO_20 = NOW.addHours(-20);
		final DateTime HOURS_AGO_21 = NOW.addHours(-21);
		final DateTime HOURS_AGO_22 = NOW.addHours(-22);
		final DateTime HOURS_AGO_23 = NOW.addHours(-23);
		final DateTime LAST_PROCESSED = lastProcessed.Value__c;
		System.debug('Query: '+soql);
		if(! hasWork()) {
			finish(bc);
		}
		return Database.getQueryLocator(soql);
	}

	/**
	 * execute method impl for Database.Batchable interface
	 *
	 * @param 	bc batchable content
	 * @param 	objectList to delete
	 */
	global void execute(
		Database.BatchableContext bc, 
		List<SObject> objectList)
	{
		if(! String.isBlank(eligibleField)) {
			final Set<Id> keys = new Map<Id,SObject>(objectList).keySet();
			// this is a saftey feature.  We won't delete any object that is not marked as eligible
			String query = 'select Id from '+objectList[0].getSObjectType().getDescribe().getName()+' where '+eligibleField+' = true and Id in :keys';
			System.debug(query);
			objectList = Database.query(query);
		}
		for(Database.DeleteResult r : Database.delete(objectList,false)) {
			SObject o = objectList.remove(0);
			for(Database.Error e : r.getErrors()) {
				errorList.add('Failed to delete '+o+': '+e);
			}
		}
		// executeNoEmail( oppHeardeStagingList );
		// email any errors that resulted in an uncaught exception
		if(! errorList.isEmpty()) {
			super.finish(lookupJob(bc));
		}
	}

	/**
	 * finish method
	 *
	 * @param job the async apex job
	 */
	global override void finish(AsyncApexJob job) {
		try {
			if(lastProcessedKey != null && errorList.isEmpty()) {
				DateTime lastProcessedDateTime = DateTime.now().addMinutes(-5);
				if(job != null && job.CreatedDate != null) {
					lastProcessedDateTime = job.CreatedDate;
				}
				if(lastProcessed.Value__c < lastProcessedDateTime) {
					lastProcessed.Value__c = lastProcessedDateTime;
					upsert lastProcessed;
				}
			}
		}
		catch(Exception e) {
			errorList.add('Failed to update lastProcessed '+e+'\n'+e.getStackTraceString());
		}
		super.finish(job);
	}

}

 

And here is an example json constructor from the screen shot in my previous post:

{
"lastProcessedKey": "APTS_AssetQueue.Cleanup",
"eligibleField": "APTS_Cleanup_Eligible__c",
"fromObject": "APTS_AssetQueue__c"
}

This particular job we don't run more than once an hour.  If someone wanted to run this more frequently, they would probably want to add constants for MINUTES.