What are task inputs and outputs in Gradle, and how do you declare them for an ad-hoc task using the runtime API?
answer
- inputs.file/dir/property
- outputs.file/dir
- fingerprint = up-to-date + cache key
- undeclared = stale/broken
- ad-hoc = runtime API
basics
~10 sInputs 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 sEvery 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 linestasks.register("copyDocs") {
inputs.dir("src/docs")
inputs.property("buildType", "release")
outputs.dir(layout.buildDirectory.dir("docs"))
doLast { /* copy logic */ }
}go deeper
Name the three input methods (file/dir/property) and two output methods (file/dir) and that they enable Gradle to skip unchanged work.
Explain the fingerprint and that the same fingerprint drives both up-to-date and the cache key; use lazy layout providers.
Stress correctness: undeclared I/O silently breaks work-avoidance; reason about when property vs file is right.
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).