The short version: you called Database.executeBatch from somewhere the platform forbids it. The message lists the forbidden places explicitly, and finish() is not among them. That is the whole fix: move the call to the finish method.
System.AsyncException: Database.executeBatch cannot be called from
a batch start, batch execute, or future method.
Why the restriction exists
A batch job’s execute method runs once per chunk. A job over 100,000 records with the default scope runs it 500 times. If each of those executions could start another batch job, one executeBatch in the wrong method would start 500 jobs, each of which could start 500 more.
The org also allows only five batch jobs to be running or queued at once. Everything beyond that sits in the Apex flex queue, which holds up to 100. A recursive batch launch would fill both in seconds and lock out every other job in the org, including ones belonging to other teams and other packages.
So the platform refuses at the point of the call rather than letting you discover the consequences at scale. The same reasoning covers start, which runs once but runs before the job is properly under way, and future methods, which have no natural place to put a successor.
The correct pattern: chain from finish()
finish() runs exactly once, after every chunk has completed. One job, one successor, no fan-out. This is the supported way to run jobs in sequence.
global class StageOneBatch implements Database.Batchable<SObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id FROM Account WHERE Needs_Sync__c = true');
}
global void execute(Database.BatchableContext bc, List<Account> scope) {
// do the work
}
global void finish(Database.BatchableContext bc) {
Database.executeBatch(new StageTwoBatch(), 200);
}
}
The BatchableContext gives you the job ID, so finish can query AsyncApexJob and decide whether the next stage should run at all:
AsyncApexJob job = [SELECT Status, NumberOfErrors, JobItemsProcessed
FROM AsyncApexJob WHERE Id = :bc.getJobId()];
if (job.NumberOfErrors == 0) {
Database.executeBatch(new StageTwoBatch(), 200);
} else {
notifyOwner(job);
}
Launching stage two unconditionally when stage one failed half its chunks is a good way to turn one incident into two.
Self-chaining, and the guard it needs
A batch job can start a fresh instance of itself from finish. This is how people process work that keeps arriving, or reprocess records that the first pass could not complete. It is also how orgs end up with a job that has been running since March.
Guard it with a condition that will eventually become false, and make that condition about the data rather than about a counter you increment:
global void finish(Database.BatchableContext bc) {
Integer left = [SELECT COUNT() FROM Account WHERE Needs_Sync__c = true];
if (left > 0 && !Test.isRunningTest()) {
Database.executeBatch(new StageOneBatch(), 200);
}
}
If the job clears the flag it filters on, the count drops and the chain ends. If it does not clear the flag, you have built an infinite loop, and the Test.isRunningTest check means you will not find out from a unit test.
What is allowed to call executeBatch
- Synchronous Apex. A trigger, a controller, anonymous execution, an invocable method. Subject to the concurrent job limits.
- Scheduled Apex. The standard way to run a nightly job: a schedulable whose
executecallsexecuteBatch. - A batch job’s finish method. As above.
- A Queueable. A Queueable is not on the forbidden list, and starting a batch from one is a common way to bridge between the two models.
Apex Scheduler has its own limit on concurrently scheduled jobs, so an org that schedules dozens of small jobs can hit a ceiling that has nothing to do with batch at all. Keeping the schedule sparse and chaining from finish is generally easier to reason about than scheduling each stage a guessed number of minutes apart.
Frequently asked
Can finish() start more than one batch job? It can, but each of them counts against the five concurrent jobs, and a job started from finish goes into the flex queue if the org is already busy. One successor is the pattern that stays predictable.
Does this apply to Queueable too? A Queueable can enqueue one further Queueable, which is a separate limit with a separate message. The two chaining models are independent.
How do I test a chain? Test each job on its own inside Test.startTest() and Test.stopTest(). Chained jobs do not run in a test context, which is why the guard clause above exists.
What if I need to react to a job finishing from outside Apex? Query AsyncApexJob for the status, or attach a Transaction Finalizer to a Queueable if the work fits that model. There is no callback into external systems on job completion beyond what you write yourself.
