skip to content

What is an incremental task in Gradle, and why would you author one instead of relying on Gradle's normal UP-TO-DATE checking?

level: middleimportance: must knowfreq 55%

answer

  1. @Incremental on file input
  2. InputChanges param on @TaskAction
  3. getFileChanges -> ChangeType ADD/MODIFY/REMOVE
  4. isIncremental false => all ADDED
  5. delete output on REMOVED

basics

~20 s

A task that, when re-run, processes only the input files that changed since the last run instead of all of them. You author one to avoid redoing work for the whole input set when a few files changed.

solid answer

~40 s

Gradle's standard incremental build works at task granularity: if any input changes, the whole task action re-runs. An incremental task goes finer — it inspects exactly which input files were added, modified, or removed and processes only those. You declare an `org.gradle.work.InputChanges` parameter on your `@TaskAction` method and mark a file input property with `@Incremental`. Then `inputChanges.getFileChanges(theProperty)` yields `FileChange` records carrying a `ChangeType` (ADDED/MODIFIED/REMOVED) and the file. You author one when per-file work is expensive (code generation, transpiling, image processing) and the input set is large but usually only a few files change between runs, so processing the delta is far cheaper than reprocessing everything.

code

kotlin · 17 lines
kotlin
abstract class Transpile : DefaultTask() {
    @get:Incremental
    @get:InputDirectory
    abstract val sources: DirectoryProperty

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction
    fun run(changes: InputChanges) {
        changes.getFileChanges(sources).forEach { c ->
            val out = outputDir.file(c.normalizedPath).get().asFile
            if (c.changeType == ChangeType.REMOVED) out.delete()
            else transpile(c.file, out)
        }
    }
}

go deeper

for a junior

Know that it processes only changed files and needs an InputChanges parameter; rough idea is enough.

for a middle

Explain @Incremental + InputChanges + getFileChanges + ChangeType, and why per-file deltas beat full reruns for expensive work.

for a senior

Stress the isIncremental/all-ADDED fallback, deterministic input→output mapping, and handling REMOVED to avoid stale outputs.

for a principal

Frame when incrementality is worth the complexity vs. simpler UP-TO-DATE, and the maintenance cost of correct delta handling across a plugin used org-wide.

## The two layers of "incremental" Gradle's incremental build at the **task** level is coarse: Gradle snapshots each task's declared inputs and outputs. If nothing changed, the task is `UP-TO-DATE` and skipped entirely; if anything changed, the **entire** task action runs again. That is great when the task is cheap or when changing one input genuinely invalidates all output. An **incremental task** is finer-grained: it lets the task action see *which specific input files changed* so it can reprocess only those and leave the rest of its outputs alone. This is a property of the task's *implementation*, not a separate task type. ## The API Two pieces wire it up: 1. Mark the file input property with `@Incremental` (from `org.gradle.work.Incremental`). This tells Gradle to track per-file changes for that property. 2. Add an `InputChanges` parameter to the `@TaskAction` method. Gradle injects it. Inside the action you call `inputChanges.getFileChanges(inputProperty)` which returns an `Iterable<FileChange>`. Each `FileChange` exposes `getFile()`, `getChangeType()` (an enum `ChangeType.ADDED`, `MODIFIED`, or `REMOVED`), `getNormalizedPath()`, and `getFileType()`. ## The isIncremental fallback — the part people forget `inputChanges.isIncremental` is `false` whenever Gradle **cannot** provide a reliable delta — first run, output directory missing, a non-`@Incremental` input changed, the task's code changed, the build cache served a different result, etc. When it is `false`, `getFileChanges` reports **every** input file as `ADDED`. Your code must therefore treat a non-incremental run as a full rebuild. The robust pattern is: on `MODIFIED`/`ADDED` regenerate the corresponding output; on `REMOVED` delete the stale output. You usually do **not** need to special-case `isIncremental` separately, because all-ADDED already produces a full rebuild — but you should clear the output directory yourself when you cannot map inputs to outputs deterministically. ## Worked example ```kotlin abstract class Transpile : DefaultTask() { @get:Incremental @get:PathSensitive(PathSensitivity.RELATIVE) @get:InputDirectory abstract val sources: DirectoryProperty @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun execute(inputChanges: InputChanges) { inputChanges.getFileChanges(sources).forEach { change -> if (change.fileType == FileType.DIRECTORY) return@forEach val target = outputDir.file(change.normalizedPath).get().asFile when (change.changeType) { ChangeType.REMOVED -> target.delete() else -> transpile(change.file, target) // ADDED or MODIFIED } } } } ``` Key correctness rules: only ask `getFileChanges` for a property annotated `@Incremental`; map each input deterministically to its output (so REMOVED can delete the right thing); and never assume the delta is non-empty or that you are on an incremental run.

  • When does Gradle decide it cannot give you an incremental delta?
    First execution, missing/changed output dir, a non-@Incremental input changing, task implementation change, or a different build-cache hit. Then isIncremental is false and every file is reported as ADDED.
  • Why must you handle REMOVED explicitly?
    Because Gradle does not delete your outputs for you. If a source file is deleted you must delete its generated output, or you'll leave stale files that pollute the output directory and break reproducibility.

Like a translator who, instead of re-translating the whole book each edition, only retranslates the paragraphs the author actually edited — and crosses out the ones that were deleted.

saying these in an interview costs you the question

  • Saying an incremental task is a special task type rather than an implementation pattern.
  • Assuming getFileChanges only ever returns the changed subset — it returns all-ADDED on non-incremental runs.
  • Forgetting to delete outputs for REMOVED inputs.

context