How does declaring a task's outputs as another task's inputs let Gradle infer task dependencies without an explicit dependsOn?
answer
- output provider carries its producer
- pass TaskProvider/Provider into inputs => implicit edge
- resolved File loses the link
- config cache flags missing implicit dependency
- dependsOn only for ordering w/o data
basics
~10 sIf you wire a producer task's output (or its output Provider) into a consumer task's input, Gradle sees the producer creates that file and automatically runs it first — no explicit dependsOn needed.
solid answer
~50 sWhen you pass a task or its output property as another task's input, Gradle records an *implicit task dependency* from the consumer to the producer. The mechanism is provider-based: a task output declared via `outputs.file(provider)` carries a 'producer' reference, and when that same provider (or the task itself) is handed to `inputs.file(...)`/`inputs.files(...)`, Gradle resolves it back to the producing task and adds the edge to the task graph. So the producer is guaranteed to run before the consumer. This is why modern Gradle code rarely uses `dependsOn` for build-step wiring — you connect data, and ordering falls out. The anti-pattern is passing a *resolved* `File` or a plain path string instead of the live provider: then the producer reference is lost and you must add `dependsOn` manually (or risk a missing-dependency / non-deterministic-ordering error, which the configuration cache will flag).
code
kotlin · 9 linesval produce = tasks.register("produce") {
val out = layout.buildDirectory.file("data.txt")
outputs.file(out)
doLast { out.get().asFile.writeText("hi") }
}
tasks.register("consume") {
inputs.files(produce) // implicit dependsOn(produce)
doLast { /* read the produced file */ }
}go deeper
Know that wiring a producer's output into a consumer's input makes Gradle run the producer first.
Explain the provider-carries-producer mechanism and why a resolved File breaks it.
Discuss configuration-cache implicit-dependency errors and when explicit dependsOn is still appropriate.
Establish lazy, provider-based wiring as the team standard to keep task graphs correct and cache-friendly at scale.
## The idea: wire data, not order Gradle's task graph can be built from data flow. If task **B** consumes a file that task **A** produces, B depends on A. Rather than writing `dependsOn("A")` by hand, you let Gradle *infer* the edge from the input/output wiring. ## How inference works The key is **carrying the producer with the value**. When a producer declares an output through a lazy provider, that provider remembers which task created it: ```kotlin val produce = tasks.register("produce") { val out = layout.buildDirectory.file("data.txt") outputs.file(out) doLast { out.get().asFile.writeText("hi") } } tasks.register("consume") { // Hand the producer (or its output) to the consumer's input: inputs.files(produce) // <-- implicit dependency on 'produce' doLast { /* read produce.get().outputs.files */ } } ``` Because `produce` (a `TaskProvider`) is passed to `inputs.files`, Gradle adds `consume dependsOn produce` automatically. The same holds when you pass an output `Provider<RegularFile>` from one task into the next task's input. ## Why the provider matters The dependency information travels **inside** the provider/`FileCollection`. If you eagerly resolve it: ```kotlin // ANTI-PATTERN: producer reference is lost val f = produce.get().outputs.files.singleFile // resolved File, no producer link tasks.register("consume2") { inputs.file(f) // Gradle no longer knows 'produce' made it } ``` Gradle sees only a bare path with no producing task, so it won't order them. At best you get non-deterministic results; with the configuration cache or `validateTaskProperties`, Gradle raises an *implicit dependency* error and tells you to add `dependsOn` or use a proper provider. ## Practical guidance - Prefer passing the `TaskProvider`, an output `Provider`, or the task's `outputs` directly into the consumer's `inputs`. - Avoid `.get()`/`.singleFile`/`File` at configuration time. - Reach for explicit `dependsOn` only for *ordering without data flow* (rare) — most wiring should be inferred. ## Relationship to outputs Inference works *because* the producer declared `outputs.file(...)`. Without an output declaration there's nothing to wire, and the consumer can't infer who makes its input.
- What error does Gradle raise if you wire a resolved File instead of a provider?Gradle reports an implicit dependency / missing task dependency problem (surfaced especially with the configuration cache or task-property validation), naming the producer it found and asking you to declare an explicit dependsOn or pass the live provider.
- When is explicit dependsOn still legitimate?For ordering that isn't driven by data flow — e.g. a lifecycle/aggregation task that must run after another for sequencing reasons, where no output feeds an input. For data wiring, inference is preferred.
Like a kitchen ticket that names the chef: hand the dish ticket to the waiter and they know who must finish cooking first; photocopy just the dish name and the chef link is gone.
saying these in an interview costs you the question
- Calling .get() / .singleFile at configuration time and losing the producer link.
- Claiming you always need dependsOn — inference removes most of it.
- Thinking inference works without the producer declaring outputs.