skip to content

In a continuous build, a teammate edits a config file consumed by a custom task but the build does not re-run. Diagnose the cause and explain how task input declarations govern continuous build triggering.

level: seniorimportance: must knowfreq 30%

answer

  1. watched set = union of declared inputs
  2. undeclared input → no watch, no rebuild
  3. same bug breaks up-to-date/cache
  4. @InputFile / inputs.file(...)
  5. RegularFileProperty via Provider

basics

~20 s

Continuous build only watches files declared as task inputs. If the custom task reads the config file without declaring it via @InputFile/inputs.file(...), Gradle doesn't watch it, so editing it triggers nothing. Add the input declaration to fix it.

solid answer

~40 s

Continuous build re-runs only when a **declared input** of an executed task changes. If a custom task reads a file but never declares it — e.g. it opens the path directly in its action instead of registering `@InputFile`/`@InputDirectory` or `inputs.file(...)` — Gradle has no knowledge that the file matters. It is excluded from the watched set, so edits produce no re-run (and, separately, the task is also wrongly cached/up-to-date). The fix is to make the input explicit: annotate the property (`@get:InputFile val config: RegularFileProperty`) wired through a `Provider`, or call `inputs.file(configFile)` in the task configuration. This both makes continuous build trigger correctly **and** makes incremental/up-to-date checking and the build cache correct. The same logic explains why generated-but-undeclared inputs or inputs added dynamically inside the task action are missed.

code

kotlin · 12 lines
kotlin
abstract class GenerateTask : DefaultTask() {
    @get:InputFile abstract val config: RegularFileProperty
    @get:OutputFile abstract val output: RegularFileProperty
    @TaskAction fun run() {
        output.get().asFile.writeText(config.get().asFile.readText())
    }
}

tasks.register<GenerateTask>("generate") {
    config.set(layout.projectDirectory.file("src/gen/config.json"))
    output.set(layout.buildDirectory.file("gen/out.txt"))
}

go deeper

for a junior

Recognize that you must declare a file as a task input for Gradle to watch it.

for a middle

Show the @InputFile / inputs.file(...) fix and explain the watched-set = declared-inputs rule.

for a senior

Connect the missed-trigger symptom to the deeper up-to-date/build-cache correctness bug and use Provider-based typed inputs.

for a principal

Set conventions/lint for input correctness across custom tasks and plugins so continuous build and caching are trustworthy team-wide.

## The root cause: the watched set = declared inputs Continuous build computes the files to watch as the **union of declared inputs** of the tasks that executed. If a file is read by a task but **not declared** as an input, Gradle literally does not know it is relevant — so: 1. It is **not watched** → editing it in continuous mode triggers nothing. 2. It is **not part of up-to-date/cache keys** → the task may be wrongly `UP-TO-DATE` or `FROM-CACHE` even in normal builds. Both symptoms have the **same fix**: declare the input. ## Declaring inputs the modern way Prefer **typed lazy properties** wired through the Provider API: ```kotlin abstract class GenerateTask : DefaultTask() { @get:InputFile abstract val config: RegularFileProperty @get:OutputFile abstract val output: RegularFileProperty @TaskAction fun run() { output.get().asFile.writeText(transform(config.get().asFile.readText())) } } tasks.register<GenerateTask>("generate") { config.set(layout.projectDirectory.file("src/gen/config.json")) output.set(layout.buildDirectory.file("gen/out.txt")) } ``` Now `config.json` is a declared `@InputFile`, so continuous build watches it and re-runs on edit. ## The runtime alternative For ad-hoc tasks you can use the runtime API: ```kotlin tasks.register("generate") { val configFile = layout.projectDirectory.file("src/gen/config.json") inputs.file(configFile) doLast { /* read configFile */ } } ``` ## Common ways inputs get missed - Reading a path with raw `File("...")` inside `@TaskAction` without declaring it. - Declaring a **directory** but the real input is a sibling file outside it. - Inputs computed/added **after** configuration, or behind a flag, so they aren't in the static declaration. - Treating a build-script constant as an input (script edits aren't watched like source inputs). ## Why this is a senior-level signal It ties three Gradle concepts together — **continuous build triggering**, **incremental up-to-date checking**, and **build-cache correctness** — all driven by the *same* input/output declarations. Diagnosing 'it didn't re-run' as 'an under-declared input' (not 'watching is broken') is the key insight.

  • Besides breaking continuous build, what other correctness problem does the same under-declared input cause?
    Up-to-date checking and the build cache use the declared inputs as the cache key. An undeclared input means the task can be reported UP-TO-DATE or restored FROM-CACHE even though its real input changed — a stale-output bug.
  • Why is annotating a RegularFileProperty preferable to calling inputs.file(...) at runtime?
    Typed lazy properties integrate with the Provider API, support task-output wiring and lazy configuration, and are required for configuration-cache compatibility; the runtime API is fine for ad-hoc tasks but less robust.

saying these in an interview costs you the question

  • Blaming file-system watching or the daemon instead of the missing input declaration.
  • Suggesting `outputs.upToDateWhen { false }` or rerunning manually as the fix rather than declaring the input.
  • Reading input files via raw File() in the task action with no declaration.

context