jenkinsci/jenkins · error · IllegalStateException

Cannot build {0} because its configuration has not been save

Error message

Cannot build {0} because its configuration has not been saved.

What it means

Thrown by `BuildCommand.run()` when the target job reports `!job.isBuildable()` and `job.isHoldOffBuildUntilSave()` is true, resolving the localized message `BuildCommand_CLICause_CannotBuildConfigNotSaved`. Jenkins marks a job as 'hold off until save' after its config.xml is written (e.g. via the POST config.xml REST endpoint or direct disk edit) without a programmatic `save()`, so it refuses to queue builds until the config is re-saved through the UI or API. The error surfaces as an IllegalStateException from the CLI `build` command.

Source

Thrown at core/src/main/java/hudson/cli/BuildCommand.java:164

            SCMTriggerItem item = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(job);
            if (item == null)
                throw new AbortException(job.getFullDisplayName() + " has no SCM trigger, but checkSCM was specified");
            // preemptively check for a polling veto
            if (SCMDecisionHandler.firstShouldPollVeto(job) != null) {
                return 0;
            }
            if (!item.poll(new StreamTaskListener(stdout, getClientCharset())).hasChanges())
                return 0;
        }

        if (!job.isBuildable()) {
            String msg = Messages.BuildCommand_CLICause_CannotBuildUnknownReasons(job.getFullDisplayName());
            if (job instanceof ParameterizedJobMixIn.ParameterizedJob && ((ParameterizedJobMixIn.ParameterizedJob) job).isDisabled()) {
                msg = Messages.BuildCommand_CLICause_CannotBuildDisabled(job.getFullDisplayName());
            } else if (job.isHoldOffBuildUntilSave()) {
                msg = Messages.BuildCommand_CLICause_CannotBuildConfigNotSaved(job.getFullDisplayName());
            }
            throw new IllegalStateException(msg);
        }

        Queue.Item item = ParameterizedJobMixIn.scheduleBuild2(job, 0, new CauseAction(new CLICause(Jenkins.getAuthentication2().getName())), a);
        QueueTaskFuture<? extends Run<?, ?>> f = item != null ? (QueueTaskFuture) item.getFuture() : null;

        if (wait || sync || follow) {
            if (f == null) {
                throw new IllegalStateException(BUILD_SCHEDULING_REFUSED);
            }
            Run<?, ?> b = f.waitForStart();    // wait for the start
            stdout.println("Started " + b.getFullDisplayName());
            stdout.flush();

            if (sync || follow) {
                try {
                    if (consoleOutput) {
                        // read output in a retry loop, by default try only once
                        // writeWholeLogTo may fail with FileNotFound

View on GitHub (pinned to 2e228ff40b)

Solutions

  1. Open the job in the Jenkins UI and click 'Save' (or POST to /job/<job>/config.xml with the full config plus a proper save), which clears the hold-off flag.
  2. Programmatically call `job.save()` (Jenkins API) after any config change so isHoldOffBuildUntilSave() returns false before invoking the build command.
  3. Verify with `job.isBuildable()` and `job.isHoldOffBuildUntilSave()` in a script before submitting the CLI build, and surface a clear message instead of letting the IllegalStateException escape.

Example fix

// before (REST edits config without saving):
//   curl -X POST --data-binary @config.xml $JENKINS/job/myjob/config.xml
//   java -jar jenkins-cli.jar build myjob   // fails: config not saved
//
// after: also trigger a real save via the UI 'Save' button, or in code:
//   TopLevelItem job = Jenkins.get().getItemByFullName("myjob", AbstractProject.class);
//   job.updateByXml(/* new config */);
//   job.save();   // clears hold-off-build-until-save
//   java -jar jenkins-cli.jar build myjob
Defensive patterns

Strategy: validation

Validate before calling

// Before invoking the CLI build, ensure the job is buildable and not held off
// Job<? extends Job,? extends Build> job = jenkins.getItemByFullName(name, Project.class);
if (job == null) throw new IllegalStateException("No such job: " + name);
if (!job.isBuildable()) {
    if (job instanceof ParameterizedJobMixIn.ParameterizedJob
            && ((ParameterizedJobMixIn.ParameterizedJob) job).isDisabled())
        throw new IllegalStateException(job.getFullDisplayName() + " is disabled");
    if (job.isHoldOffBuildUntilSave()) {
        job.save();   // clear the hold-off flag first
    } else {
        throw new IllegalStateException(job.getFullDisplayName() + " is not buildable");
    }
}
// safe to build now

Prevention

When it happens

Trigger: Running `java -jar jenkins-cli.jar build <job>` (plain or with -w/-s/-f) against a job whose `config.xml` was modified outside the normal save flow. Specifically the code path at line 157-164: `isBuildable()` is false and the `isHoldOffBuildUntilSave()` branch wins over the disabled branch.

Common situations: Editing a job's config.xml on disk and reloading, using the Jenkins REST `POST /job/<job>/config.xml` which sets the hold-off flag, or a plugin that mutates config without calling `Item.save()`. Also after a Jenkins restart that reloads jobs from disk without re-saving them.

Related errors


AI-assisted analysis of jenkinsci/jenkins@2e228ff40b (2026-08-14). Data as JSON: /api/errors/dec08dff67db50d5. Report an issue: GitHub.