skip to content

What does InputChanges.isIncremental mean, and how should an incremental task behave when it is false?

level: seniorimportance: should knowfreq 35%

answer

  1. true = trustworthy per-file delta
  2. false = first run / output gone / non-@Incremental input changed
  3. false => getFileChanges all ADDED
  4. ADDED branch = full regen for free
  5. explicit check only to purge outputs

basics

~20 s

isIncremental is true only when Gradle can give a reliable per-file delta. When false (first run, missing outputs, a non-@Incremental input changed), getFileChanges reports every file as ADDED, so your action effectively performs a full rebuild.

solid answer

~50 s

`inputChanges.isIncremental` tells you whether Gradle could compute a trustworthy delta of file changes. It is `false` on the first run, when the task's outputs were removed or changed outside the build, when a non-`@Incremental` input property changed, when the task's implementation (class/inputs) changed, or after certain cache scenarios. When it is `false`, Gradle still lets you call `getFileChanges`, but it reports **all** input files as `ADDED` — so a correctly written action that treats ADDED as 'generate output' automatically degrades to a full rebuild. The practical guidance: write the action so the ADDED branch is a complete regeneration, and you rarely need to branch on `isIncremental` explicitly. You *do* read it when you must clean the output directory up front (e.g., outputs that can't be mapped 1:1 from inputs) — then you clear outputs only on a non-incremental run and trust the per-file delta otherwise.

code

kotlin · 10 lines
kotlin
@TaskAction
fun execute(inputChanges: InputChanges) {
    if (!inputChanges.isIncremental) {
        project.delete(outputDir)   // safe reset on full rebuild
    }
    inputChanges.getFileChanges(sources).forEach { c ->
        val out = outputDir.file(c.normalizedPath).get().asFile
        if (c.changeType == ChangeType.REMOVED) out.delete() else generate(c.file, out)
    }
}

go deeper

for a junior

Know it means 'is this an incremental run', and false ~ first run.

for a middle

List the main reasons it goes false and that false => all files reported ADDED.

for a senior

Explain the all-ADDED contract giving free full-rebuild semantics and when to explicitly purge outputs.

for a principal

Reason about correctness guarantees and the failure modes of authors who over-purge or mis-handle non-incremental runs in a widely reused plugin.

## What isIncremental actually signals `InputChanges.isIncremental` is a boolean: `true` means Gradle has a previous-execution snapshot it trusts and the only things that changed are `@Incremental` file inputs, so it can hand you an accurate ADD/MODIFY/REMOVE delta. `false` means Gradle cannot, and is falling back to treating the run as a full rebuild. ### Common reasons it is false - **First execution** — no prior snapshot exists. - **Output out of date / missing** — the `@OutputDirectory` was deleted or modified by something other than this task, so Gradle can't assume the prior outputs are intact. - **A non-incremental input changed** — any input property *not* annotated `@Incremental` (a `@Input` flag, a `@Classpath`, a non-`@Incremental` file property) changed. Such inputs can affect *every* output, so Gradle invalidates the whole task. - **Implementation changed** — the task class or an `@Input` action property changed. - **Cache restore** — outputs restored from the build cache for a different key. ## The crucial invariant: all-ADDED on non-incremental When `isIncremental` is `false`, `getFileChanges(prop)` returns every current input file as `ChangeType.ADDED` (and no REMOVED entries, since there's no trusted prior state). This is deliberate: a task whose ADDED branch fully regenerates the corresponding output will, on a non-incremental run, regenerate **everything** — exactly the full-rebuild semantics you want. So most well-authored tasks never explicitly test `isIncremental`. ## When you do need to branch on it ```kotlin @TaskAction fun execute(inputChanges: InputChanges) { if (!inputChanges.isIncremental) { project.delete(outputDir) // can't map outputs 1:1, so start clean } inputChanges.getFileChanges(sources).forEach { change -> val out = outputDir.file(change.normalizedPath).get().asFile when (change.changeType) { ChangeType.REMOVED -> out.delete() else -> generate(change.file, out) } } } ``` You clear the output directory **only** when `isIncremental` is false — because on a non-incremental run you've lost the ability to reason about what's stale, so wiping and regenerating is the safe reset. On an incremental run you must *not* wipe, or you'd destroy outputs for unchanged inputs that you won't regenerate. ## Mental model Think of `isIncremental` as 'can I trust the delta?'. `true` → process only the delta. `false` → the delta is the whole world; behave like a clean build. The all-ADDED contract means you usually get correct full-rebuild behavior for free, and you only reach for the explicit check when you must proactively purge outputs.

  • Why does changing a non-@Incremental input force isIncremental to false?
    Gradle can't reason about which outputs that input affects — it could change all of them — so it can't trust a per-file file delta and falls back to a full rebuild for correctness.
  • If most tasks don't need to read isIncremental, why does the API expose it?
    For tasks whose outputs can't be deterministically mapped from inputs: they must proactively clear the output directory on a non-incremental run, which requires explicitly testing the flag.
  • Will you ever see REMOVED entries when isIncremental is false?
    No. With no trusted prior snapshot Gradle reports only ADDED for all current inputs; there is no reliable prior state to derive REMOVED from.

saying these in an interview costs you the question

  • Saying you must always branch on isIncremental to get a full rebuild — the all-ADDED contract handles it.
  • Clearing the output directory on every run, which defeats incrementality.
  • Believing isIncremental stays true when a plain @Input flag changes.

context