flowable/flowable-engine · error · FlowableException
Empty external worker job can not be scheduled
Error message
Empty external worker job can not be scheduled
What it means
DefaultJobManager.moveExternalWorkerJobToExecutableJob converts an ExternalWorkerJobEntity (locked external-worker job) into a regular executable JobEntity. A null external worker job cannot be scheduled, so the method throws this FlowableException as a fail-fast precondition.
Solutions
- Null-check externalWorkerJob before calling the method and skip/log on null
- Fix upstream acquisition code that yields null entities
- Catch the FlowableException and re-fetch the job state to handle lifecycle races
Example fix
// before
jobManager.moveExternalWorkerJobToExecutableJob(externalWorkerJob);
// after
if (externalWorkerJob != null) {
jobManager.moveExternalWorkerJobToExecutableJob(externalWorkerJob);
} Defensive patterns
Strategy: validation
Validate before calling
if (externalWorkerJob == null) { log.warn("No external worker job to move"); return; } Type guard
boolean canSchedule(ExternalWorkerJobEntity j) { return j != null && j.getId() != null; } Try / catch
try { jobManager.moveExternalWorkerJobToExecutableJob(job); } catch (FlowableException e) { log.warn("External worker job absent before move: {}", e.getMessage()); } Prevention
- Null-check external-worker job lookups before moving
- Account for concurrent completion/deletion by workers
- Guard custom external-worker acquisition code at boundaries
When it happens
Trigger: Passing null to JobManager.moveExternalWorkerJobToExecutableJob(externalWorkerJob), usually when an external-worker job fetch or lock result was null and passed through unchecked.
Common situations: Custom external-worker acquisition/timeout handling code; races where the external worker job was already completed or deleted before the move.
Related errors
- Empty timer job can not be scheduled
- Null job provided
- Provided correlationId is null
- activity tenant id is null
- activityId is null
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/3d4a9d2780f71ad5.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/DefaultJobManager.java:167
// Only hint when there is enough capacity remaining in the job queue
boolean remainingCapacitySufficient = isAsyncExecutorRemainingCapacitySufficient(timerJobEntities.size());
for (TimerJobEntity timerJobEntity : timerJobEntities) {
JobEntity executableJob = createExecutableJobFromOtherJob(timerJobEntity, remainingCapacitySufficient);
boolean insertSuccessful = jobServiceConfiguration.getJobEntityManager().insertJobEntity(executableJob);
if (insertSuccessful && remainingCapacitySufficient) {
triggerExecutorIfNeeded(executableJob);
}
}
jobServiceConfiguration.getTimerJobEntityManager().bulkDeleteTimerJobsWithoutRevisionCheck(timerJobEntities);
}
@Override
public JobEntity moveExternalWorkerJobToExecutableJob(ExternalWorkerJobEntity externalWorkerJob) {
if (externalWorkerJob == null) {
throw new FlowableException("Empty external worker job can not be scheduled");
}
JobEntity executableJob = createExecutableJobFromOtherJob(externalWorkerJob);
// This job should now become a regular async job
fillDefaultAsyncJobInfo(executableJob, executableJob.isExclusive());
boolean insertSuccessful = jobServiceConfiguration.getJobEntityManager().insertJobEntity(executableJob);
if (insertSuccessful) {
jobServiceConfiguration.getExternalWorkerJobEntityManager().delete(externalWorkerJob);
triggerExecutorIfNeeded(executableJob);
return executableJob;
}
return null;
}
@Override
public TimerJobEntity moveJobToTimerJob(AbstractRuntimeJobEntity job) {
TimerJobEntity timerJob = createTimerJobFromOtherJob(job);
boolean insertSuccessful = jobServiceConfiguration.getTimerJobEntityManager().insertTimerJobEntity(timerJob);View on GitHub (pinned to d6d39ce1c6)