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

  1. Null-check externalWorkerJob before calling the method and skip/log on null
  2. Fix upstream acquisition code that yields null entities
  3. 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

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


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)