skip to content

Declaring Inputs And Outputs

Why a task with incomplete input or output declarations can never be safely marked up to date or cached. Interviewers use it to check that you treat declarations as a correctness contract rather than boilerplate.

on this pageshow

questions

5

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

open as a page

How exactly do missing or incorrect input/output declarations break Gradle's up-to-date checks and build cache?

level: middleimportance: must knowfreq 65%

basics

~20 s

Gradle decides up-to-date and the cache key from DECLARED inputs/outputs only. A missing input means changes go undetected (stale results); a missing output means the cache can't store/restore it, and over-declaring causes needless re-runs and cache misses.

open as a page

How do you make an ad-hoc task cacheable using the runtime API, and what role do inputs/outputs play in that?

level: middleimportance: should knowfreq 45%

basics

~20 s

Declare all real inputs and outputs, then opt in with outputs.cacheIf { true } (and optionally outputs.doNotCacheIf for exclusions). The declared inputs form the cache key; the declared outputs are what gets stored and restored.

open as a page

A task keeps producing stale results even after you change a source file. How do you confirm and fix a missing input declaration?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Run with --info to see Gradle report the task UP-TO-DATE despite your change — that confirms the changed file isn't a declared input. Add it via inputs.file()/dir()/property() so its content joins the fingerprint.

open as a page

What goes wrong when ad-hoc input/output declarations are configured eagerly or with hardcoded paths, and how do lazy Providers help?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Eager File paths and values captured at configuration time can be wrong, miss task-dependency inference, or read stale config. Using lazy Providers/Property and layout.buildDirectory defers resolution and links producer→consumer tasks automatically.

open as a page