skip to content

What does the InputChanges API give a task action that ordinary up-to-date checking does not, and when does a task receive it?

level: middleimportance: must knowfreq 55%

answer

  1. @Incremental input + InputChanges param
  2. getFileChanges -> ADDED/MODIFIED/REMOVED
  3. isIncremental true = precise delta
  4. false = everything reported ADDED
  5. you must delete outputs for REMOVED

basics

~10 s

Up-to-date checking only decides skip-or-rerun for the whole task. InputChanges tells the action exactly which input files were added, modified, or removed, so it can reprocess only those instead of everything.

solid answer

~40 s

Gradle's normal incremental-build feature is coarse: it compares input/output snapshots and decides to either skip a task entirely (UP-TO-DATE) or run it from scratch. A task with an `@Incremental`-annotated file input can instead accept an `InputChanges` parameter in its action. When inputs change in a way Gradle can track, `InputChanges.isIncremental` is `true` and `getFileChanges(input)` yields only the changed files with a `ChangeType` of ADDED, MODIFIED, or REMOVED — so the action processes just those. When Gradle can't determine a clean delta (first run, an untracked-output or non-file input changed, or output was tampered with), `isIncremental` is `false` and `getFileChanges` returns *every* file as ADDED, so the action must handle a full rebuild. Net effect: finer-grained work avoidance *inside* a single task execution.

code

kotlin · 11 lines
kotlin
@TaskAction
fun run(changes: InputChanges) {
    if (!changes.isIncremental) outputDir.get().asFile.deleteRecursively()
    changes.getFileChanges(inputDir).forEach { c ->
        val out = outputDir.file(c.normalizedPath).get().asFile
        when (c.changeType) {
            ChangeType.REMOVED -> out.delete()
            else -> process(c.file, out)
        }
    }
}

go deeper

for a junior

Know that it reports which files changed (added/modified/removed) so the task does less work.

for a middle

Explain the two modes (isIncremental true vs false) and that false reports all files as ADDED.

for a senior

Tie it to up-to-date checking granularity, the requirement to clean REMOVED outputs, and why non-incremental fallbacks occur.

for a principal

Frame when investing in incremental task actions pays off (large file sets, expensive per-file work) vs. relying on the cache, and the maintenance risk of the dual-path action.

## The problem InputChanges solves Gradle's everyday incrementality works at the **task** granularity. Before running a task, Gradle snapshots its declared inputs (files + their normalized content hashes, plus input properties) and its outputs. If the snapshot matches the previous successful run, the task is marked `UP-TO-DATE` and skipped. Otherwise the whole task action runs again. There is no middle ground at this level: change one source file in a directory of 10,000 and the task reruns over all 10,000. **Incremental tasks with `InputChanges`** add a finer layer: the task action itself receives a description of *which* files changed since the last run and can do proportional work. ## Wiring it up (consumer side) The input you want tracked at file granularity is annotated `@Incremental` (alongside `@InputFiles`/`@InputDirectory` and a normalization annotation like `@PathSensitive`). The task action then declares a parameter of type `org.gradle.work.InputChanges`: - `inputChanges.isIncremental` — `true` when Gradle could compute a precise delta; `false` when it could not. - `inputChanges.getFileChanges(fileProperty)` — returns an `Iterable<FileChange>`; each `FileChange` exposes `getFile()`, `getChangeType()` (`ChangeType.ADDED` / `MODIFIED` / `REMOVED`), `getNormalizedPath()`, and `getFileType()`. ## The two execution modes 1. **Incremental run** (`isIncremental == true`): `getFileChanges` lists *only* the files that actually changed. Your action processes ADDED/MODIFIED files and deletes the outputs corresponding to REMOVED files. This is the fast path. 2. **Non-incremental run** (`isIncremental == false`): `getFileChanges` reports *every* input file as `ADDED`. Gradle falls back to this when it cannot trust a partial delta — the very first execution, a change to a non-`@Incremental` input or an input property, an output directory that was modified outside Gradle, or a change Gradle classifies as not incrementally trackable. Your action must be written to also rebuild everything in this case (usually: clear the output directory first, then process all 'changes'). ## Why REMOVED matters Because an incremental action only touches changed inputs, it is solely responsible for cleaning up stale outputs. If `B.txt` is deleted, Gradle won't re-run the whole task — it just tells you `B.txt` is `REMOVED`, and your code must delete the derived `B.out`. Forgetting this leaves orphaned outputs. ```kotlin abstract class TransformFiles : DefaultTask() { @get:Incremental @get:PathSensitive(PathSensitivity.RELATIVE) @get:InputDirectory abstract val sources: DirectoryProperty @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun execute(changes: InputChanges) { if (!changes.isIncremental) { project.delete(outputDir) // full rebuild path } changes.getFileChanges(sources).forEach { change -> val target = outputDir.file(change.normalizedPath).get().asFile when (change.changeType) { ChangeType.REMOVED -> target.delete() else -> if (change.fileType != FileType.DIRECTORY) { target.parentFile.mkdirs() target.writeText(change.file.readText().uppercase()) } } } } } ``` ## Relationship to the build cache InputChanges is purely a *local* optimization: it relies on the previous outputs still being present in the project. A cache hit (or a clean checkout) replaces outputs wholesale and is unrelated; in fact after a cache hit there is no prior local state, so the next change triggers a non-incremental run.

  • If isIncremental is false, what does getFileChanges return?
    Every current input file, each reported with ChangeType.ADDED — Gradle's signal that you must do a full rebuild.
  • Does using InputChanges change whether the task is skipped as UP-TO-DATE?
    No. The up-to-date decision is unchanged; InputChanges only refines what happens once Gradle has already decided the task must run.

Up-to-date check is a smoke alarm: it only says 'something burned, redo the room'. InputChanges is a checklist of exactly which items got scorched, so you replace only those.

saying these in an interview costs you the question

  • Claiming InputChanges lets Gradle skip the task — it operates only after Gradle decides to run it.
  • Saying you can ignore the non-incremental (isIncremental == false) branch — that breaks first runs and after-cache-hit runs.

context