What situations cause Gradle to run an InputChanges-aware task in non-incremental mode (isIncremental == false)?
answer
- first run = non-incremental
- non-@Incremental input changed
- scalar @Input property changed
- outputs touched outside Gradle
- after cache hit / clean -> no baseline
basics
~10 sThe 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
Know the first run is always non-incremental.
List the main triggers (first run, property change, outputs tampered).
Explain why each trigger invalidates the baseline and how to structure inputs to maximize incremental runs.
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.