skip to content

What are task inputs and outputs in Gradle, and how do you declare them for an ad-hoc task using the runtime API?

level: juniorimportance: must knowfreq 70%

answer

  1. inputs.file/dir/property
  2. outputs.file/dir
  3. fingerprint = up-to-date + cache key
  4. undeclared = stale/broken
  5. ad-hoc = runtime API

basics

~10 s

Inputs are the files/values a task reads; outputs are what it produces. For ad-hoc tasks you declare them at runtime via task.inputs.file()/dir()/property() and task.outputs.file()/dir() so Gradle can track changes.

solid answer

~40 s

Every Gradle task has `inputs` and `outputs` containers. **Inputs** are everything the task depends on: source files (`inputs.file(...)`, `inputs.dir(...)`), and scalar/config values (`inputs.property("name", value)`). **Outputs** are what it produces: `outputs.file(...)` or `outputs.dir(...)`. For ad-hoc tasks (`tasks.register("x") { doLast { ... } }`) you wire these on the task object at configuration time. Gradle fingerprints declared inputs and snapshots declared outputs; on the next run it compares fingerprints to decide UP-TO-DATE, and uses the same fingerprint as the build-cache key. The golden rule: anything the task *actually* reads must be an input and anything it *writes* must be an output — otherwise Gradle's tracking is wrong.

code

kotlin · 6 lines
kotlin
tasks.register("copyDocs") {
    inputs.dir("src/docs")
    inputs.property("buildType", "release")
    outputs.dir(layout.buildDirectory.dir("docs"))
    doLast { /* copy logic */ }
}

go deeper

for a junior

Name the three input methods (file/dir/property) and two output methods (file/dir) and that they enable Gradle to skip unchanged work.

for a middle

Explain the fingerprint and that the same fingerprint drives both up-to-date and the cache key; use lazy layout providers.

for a senior

Stress correctness: undeclared I/O silently breaks work-avoidance; reason about when property vs file is right.

for a principal

Frame declaration discipline as a build-reliability invariant across a large multi-module build and how to enforce it (validation, conventions, reviews).

## What inputs and outputs are A Gradle task is a unit of work. To skip work it doesn't need to redo, Gradle must know **exactly what the task consumes (inputs)** and **what it produces (outputs)**. These are not guessed — you declare them. Each `Task` exposes two containers: `task.inputs` (a `TaskInputs`) and `task.outputs` (a `TaskOutputs`). For *ad-hoc* tasks (tasks you define inline rather than as a custom typed class), you use the **runtime API** to register them: - `inputs.file(path)` / `inputs.files(paths...)` — one or many input files. - `inputs.dir(path)` — an input directory (all files under it, recursively). - `inputs.property("name", value)` — a scalar input (String, Int, Boolean, enum, etc.). Use this for things like a version string, a flag, or an environment value the task branches on. - `outputs.file(path)` — a single produced file. - `outputs.dir(path)` — a produced directory. ## Why declaration matters Gradle creates a **fingerprint** of all declared inputs (file content hashes + property values). On the next build it recomputes the fingerprint; if nothing changed AND the declared outputs are still present and unchanged, the task is reported `UP-TO-DATE` and skipped. That same input fingerprint is also the **build-cache key**: with the cache enabled, a matching key lets Gradle pull a previous output from the local or remote cache instead of executing at all. The consequence: **undeclared inputs/outputs silently break correctness of work-avoidance.** If a task reads a file you never declared, changing that file won't re-trigger the task (stale output). If a task writes a file you never declared, the cache can't restore it and `clean` semantics break. ## Example ```kotlin tasks.register("generateConfig") { val template = layout.projectDirectory.file("src/config.template") val versionValue = project.version.toString() val out = layout.buildDirectory.file("generated/config.txt") inputs.file(template) inputs.property("version", versionValue) outputs.file(out) doLast { out.get().asFile.writeText( template.asFile.readText().replace("@version@", versionValue) ) } } ``` Here the template file, the version value, and the produced file are all declared, so Gradle can correctly skip the task when none changed. ## Lazy types Prefer Gradle's lazy `Provider`/`Property` and `layout.buildDirectory`/`projectDirectory` so paths resolve correctly and participate in task-dependency inference, rather than eager `File` objects or hardcoded strings.

  • When would you use inputs.property() instead of inputs.file()?
    For non-file scalar values the task branches on — a version string, a boolean flag, an enum mode. Changing the value should invalidate the result, but there's no file to hash, so you declare it as a property whose value is part of the fingerprint.
  • What happens at the very first run when no fingerprint exists yet?
    There is nothing to compare against, so the task is NOT up-to-date and executes; Gradle then records the input fingerprint and output snapshot for next time (and stores the result in the cache if enabled).

saying these in an interview costs you the question

  • Saying inputs/outputs are auto-detected from what the task reads/writes — Gradle does not trace I/O; you must declare it.
  • Confusing inputs.property() (a scalar value) with inputs.file() (a file path).

context