skip to content

What situations cause Gradle to run an InputChanges-aware task in non-incremental mode (isIncremental == false)?

level: seniorimportance: should knowfreq 38%

answer

  1. first run = non-incremental
  2. non-@Incremental input changed
  3. scalar @Input property changed
  4. outputs touched outside Gradle
  5. after cache hit / clean -> no baseline

basics

~10 s

The first run, a change to a non-@Incremental input or input property, output history being unavailable, or outputs modified outside Gradle. Then getFileChanges reports every file as ADDED.

solid answer

~50 s

`isIncremental` is `false` whenever Gradle cannot safely compute a partial delta against the previous run. Common triggers: (1) the **first execution** — there's no prior state to diff; (2) a change to an input that is **not** annotated `@Incremental` (any plain `@Input`/`@InputFiles` value or `@InputDirectory` without `@Incremental`), since a non-tracked input flips the whole task to full mode; (3) a change to a simple **input property** (`@Input val`); (4) **output history is missing or invalid** — e.g. the output directory was deleted or modified by something other than Gradle, or this run follows a build-cache restore so there's no incremental baseline; (5) Gradle's own task-history was cleared. In all these cases `getFileChanges` returns *every* current input file as `ChangeType.ADDED`, signalling the action to rebuild from scratch (typically: wipe the output directory, then regenerate). Only a change confined to `@Incremental` file inputs yields `isIncremental == true`.

go deeper

for a junior

Know the first run is always non-incremental.

for a middle

List the main triggers (first run, property change, outputs tampered).

for a senior

Explain why each trigger invalidates the baseline and how to structure inputs to maximize incremental runs.

for a principal

Reason about CI patterns (cache restore then incremental), input churn budgets, and when the dual-path complexity is worth it across the build.

## Why a fallback mode exists at all Incremental processing is only valid if Gradle can *prove* that the difference between this run and the last successful run is fully captured by the file changes it hands you. When that proof isn't available, applying a partial delta would silently corrupt outputs — so Gradle drops to a safe, full-rebuild mode and tells you via `isIncremental == false`. ## The trigger list 1. **First run / no history.** Nothing to diff against. Everything is 'new'. 2. **A non-`@Incremental` input changed.** Only inputs you explicitly annotate `@Incremental` are tracked at file granularity. If any other declared input changes — an `@Input` property, an `@Classpath`, an `@InputFiles` without `@Incremental` — Gradle cannot attribute the change to specific files, so the whole action goes non-incremental. (A task may have at most a bounded set of `@Incremental` inputs; a change anywhere outside them forces full mode.) 3. **An input *property* changed.** Scalar inputs (`@Input` String/Boolean/etc.) have no file-level delta, so changing one forces full mode. 4. **Output state can't be trusted.** If the `@OutputDirectory`/`@OutputFiles` were modified outside Gradle (manual edit, another tool, a partial delete), the incremental baseline is invalid and Gradle rebuilds. 5. **After a build-cache hit or `clean`.** Outputs were replaced wholesale (or removed); there's no incremental baseline, so the *next* change runs non-incrementally. 6. **Gradle version/history reset.** Upgrading Gradle or clearing the project's task history removes the prior snapshots. ## What you observe and must do ```kotlin @TaskAction fun run(changes: InputChanges) { if (!changes.isIncremental) { logger.info("Non-incremental: rebuilding all outputs") outputDir.get().asFile.deleteRecursively() // start clean } changes.getFileChanges(sources).forEach { c -> /* all ADDED here when non-incremental */ } } ``` Because `getFileChanges` reports **all** files as `ADDED` in this mode, an action that simply loops over the reported changes and regenerates each output is automatically correct for both modes — provided you also clear the output directory first so no stale files survive. ## Practical implications - Don't be surprised that editing a single `@Input` flag triggers a full reprocess; that's by design. - Mixing many non-`@Incremental` inputs that change often defeats the optimization — keep the volatile, file-level inputs `@Incremental` and minimize churny scalar inputs. - After CI restores from the remote build cache, the first local change won't be incremental; this is expected, not a bug.

  • Does a build-cache hit produce an incremental run afterward?
    No. The cache replaces outputs wholesale, leaving no incremental baseline, so the next change runs non-incrementally (all files ADDED).
  • If only one @Incremental input directory changes, is the run incremental?
    Yes — provided no other declared input or input property also changed and the output history is intact.
  • Why does changing a scalar @Input force full mode?
    Scalar properties have no per-file delta to report, so Gradle can't localize the change and must assume everything is affected.

saying these in an interview costs you the question

  • Claiming incremental mode is the default for the first run.
  • Assuming a change to any @InputFiles keeps the run incremental — only @Incremental-annotated inputs are tracked at file level.

context