Gradle Tasks, Task Graph, Dependencies, Ordering, Inputs, Outputs, and Incremental Work: Concepts, Architecture, and Mental Model
Model Gradle work as a selected task graph whose correctness depends on explicit dependencies, ordering rules, declared inputs and outputs, and truthful incremental-work contracts.
Model Gradle work as a selected task graph whose correctness depends on explicit dependencies, ordering rules, declared inputs and outputs, and truthful incremental-work contracts.
Learning objectives
- Explain registration, configuration, graph selection, and execution as separate task states.
-
Distinguish
dependsOn,mustRunAfter,shouldRunAfter, andfinalizedByby their graph effects. - Explain how declared scalar/file inputs and outputs drive up-to-date checks and can infer producer/consumer dependencies.
- Interpret common task outcome labels without confusing workspace incrementality with the build cache.
- Explain path sensitivity and why absolute-path-sensitive work is less portable.
-
Describe when true incremental processing with
InputChangesis worth the added task implementation complexity.
1. The practical problem: a fast task graph is useful only when it tells the truth
Chapter 16 established lazy task registration and Provider-based build authoring. That keeps unnecessary objects and values out of configuration. Chapter 17 asks the next question: once a task is selected, what other work must exist in the graph, what state makes the task valid, and when may Gradle safely skip it?
A task that reads an undeclared file can return a stale result. Two tasks that write the same directory can make ownership ambiguous. An ordering rule can appear to “fix” a race while still allowing a required producer to be absent from the graph. These are correctness defects disguised as performance tuning.
Chapter invariant: task speed is downstream of task truthfulness. First model dependencies, inputs, outputs, and ownership correctly; then trust Gradle’s up-to-date and incremental-work optimizations.
2. Baseline and vocabulary before task code
The chapter uses the verified Gradle 9.7.1 Wrapper
from Chapter 15, JDK 21 to run Gradle, Java 17 when JVM source is
present, and an isolated GRADLE_USER_HOME. No external
plugin or hosted service is required.
| Term | Meaning | Evidence you can inspect |
|---|---|---|
| Task registration | Adds a task definition lazily to the model. |
tasks.register(...), task listing,
configuration messages.
|
| Task configuration | Sets task properties/relationships/actions before execution. |
--info, deliberately scoped configuration
prints in the lab.
|
| Task graph selection | Gradle determines selected tasks plus strong relationships/dependencies. | Requested task paths and resulting execution order. |
| Task execution | Task actions run unless skipped/up-to-date/from cache/no source. | Console outcome and produced files. |
| Input | State whose value/content can affect task output. |
@Input, @InputFile(s), runtime
inputs... APIs.
|
| Output | State a task owns and produces. |
@OutputFile, @OutputDirectory,
runtime outputs... APIs.
|
| Incremental task | A task action that receives file-level changes rather than reprocessing every input. |
InputChanges and change types
ADDED/MODIFIED/REMOVED.
|
3. Three different questions: selection, ordering, and cleanup
flowchart LR
A[prepareInput] -->|dependsOn / producer edge| B[transform]
C[lint] -.->|mustRunAfter only| B
B -->|finalizedBy| D[cleanupScratch]
prepareInput → transform means the producer is
required for the consumer. lint → transform with
mustRunAfter says only “if both are scheduled, lint
must be first”; selecting transform alone does not add lint.
transform → cleanupScratch with
finalizedBy adds cleanup when transform is scheduled,
including failure/up-to-date cases.
| Relationship | Adds referenced task to graph? | What it guarantees | Typical use |
|---|---|---|---|
dependsOn |
Yes, strong relationship. | Dependency executes before consumer if needed. | Required producer/validation prerequisite. |
mustRunAfter |
No. | Strict order only when both tasks are scheduled. | Avoid unsafe overlap between independently selectable tasks. |
shouldRunAfter |
No. | Preferred order; Gradle may ignore it for cycles/parallel progress. | Helpful but nonessential sequencing. |
finalizedBy |
Yes when finalized task is scheduled. | Finalizer runs afterward, including when finalized task fails or is UP-TO-DATE. | Cleanup/release of temporary resources. |
A common mistake is to use mustRunAfter where the real
relationship is “B consumes A’s output.” That is not ordering; it is
a dependency/data-flow edge.
4. Data flow can infer a dependency when you wire task outputs as task inputs
Gradle can infer task dependencies from Provider-backed outputs. This is stronger than pointing two unrelated tasks at the same file path because the Provider carries producer identity.
val producer = tasks.register("produce") {
val out = layout.buildDirectory.file("generated/value.txt")
outputs.file(out)
doLast {
val file = out.get().asFile
file.parentFile.mkdirs()
file.writeText("value")
}
}
val consumer = tasks.register("consume") {
// Explicit dependency shown for beginner clarity in this chapter.
dependsOn(producer)
val input = layout.buildDirectory.file("generated/value.txt")
inputs.file(input)
doLast { println(input.get().asFile.readText()) }
}
In richer typed APIs, a consuming task property can be assigned directly from a producing task’s output Provider, allowing Gradle to infer the dependency. The key idea is still the same: model data ownership, not directory coincidence.
5. Up-to-date checking compares declared state, not everything the task happens to touch
When a task has tracked inputs and outputs, Gradle snapshots
relevant input values/content and output state. On a later build, if
the declared inputs are equivalent and outputs are present/unchanged
in the way Gradle expects, the task can be labeled
UP-TO-DATE and its actions are skipped.
> Task :prepareInput UP-TO-DATE
> Task :transformText UP-TO-DATE
> Task :verifyOutput UP-TO-DATE
BUILD SUCCESSFUL
3 actionable tasks: 3 up-to-date
This is a local workspace decision. It is not proof that a
remote/shared cache returned anything. Gradle’s Build Cache is
disabled by default; a FROM-CACHE outcome requires
caching to be enabled and the task to be cacheable. This chapter
uses UP-TO-DATE as the mandatory evidence and mentions
FROM-CACHE only to prevent terminology drift.
6. Inputs can be values or files; outputs need clear ownership
Inputs include configuration values such as a format string and files such as a source directory. Outputs are files/directories generated by the task. A task that creates no declared outputs normally cannot benefit from standard up-to-date output checks in the same way as a reproducible transformation task.
tasks.register("renderGreeting") {
val greeting = providers.gradleProperty("greeting").orElse("hello")
val source = layout.projectDirectory.file("inputs/name.txt")
val target = layout.buildDirectory.file("generated/greeting.txt")
inputs.property("greeting", greeting)
inputs.file(source).withPathSensitivity(PathSensitivity.RELATIVE)
outputs.file(target)
doLast {
val out = target.get().asFile
out.parentFile.mkdirs()
out.writeText("${greeting.get()} ${source.asFile.readText().trim()}\n")
}
}
Changing either the tracked property or input file invalidates the task. Merely changing an unrelated file should not. If the action secretly reads an undeclared file, Gradle cannot incorporate that hidden state into ordinary up-to-date decisions.
7. Path sensitivity answers: which part of a file path is semantically meaningful?
File content is not always the only input. Gradle lets you state how paths participate in snapshots. The correct choice depends on the tool’s semantics.
| Path sensitivity | Tracked path information | Use carefully when |
|---|---|---|
RELATIVE |
Path relative to the declared input root plus content. | Directory layout inside the input root matters; relocating the whole workspace should not. |
NAME_ONLY |
File/directory names but not parent paths. | Only names matter; different nested locations are semantically equivalent. |
NONE |
Ignores path names; content drives identity. | The tool truly does not care which path contains which content; collisions/ambiguity are understood. |
ABSOLUTE |
Absolute path participates. | The absolute location is genuinely semantic; this reduces relocatability and cache reuse. |
Do not choose a weaker sensitivity merely to get more cache hits. Normalization must reflect the task’s real semantics, or Gradle can incorrectly reuse work.
8. Up-to-date skipping and incremental processing solve different problems
Up-to-date skipping means the task action does not run at all when nothing relevant changed. Incremental processing means the action does run because something changed, but it can process only changed files. The latter adds implementation complexity and is most valuable for many-file transformations where file-level work is expensive.
@TaskAction
fun transform(changes: InputChanges) {
if (!changes.isIncremental) {
// First run or non-incremental invalidation: rebuild all output state.
}
changes.getFileChanges(inputDir).forEach { change ->
println("${change.changeType}: ${change.normalizedPath}")
}
}
An incremental action must have a file input marked with
@Incremental or @SkipWhenEmpty. The change
stream can report added, modified, and removed files. The task still
needs correct outputs and a strategy for non-incremental execution.
9. Read task outcomes as evidence, not decorations
| Outcome | Meaning for investigation |
|---|---|
UP-TO-DATE |
Tracked inputs/outputs indicate no action is needed in this workspace. |
FROM-CACHE |
Outputs were restored from an enabled build cache; not the same as UP-TO-DATE. |
NO-SOURCE |
A source-processing task had no source files to process. |
SKIPPED |
Execution was skipped for another reason, such as a predicate/disabled task. |
| No label | Task action executed normally. |
FAILED |
Task execution failed; preserve the original failure and relevant report/log evidence. |
10. Read-only/model inspection before changing task relationships
From a trusted build, first inspect what exists and what would be selected:
export GRADLE_USER_HOME="$PWD/.lab-gradle-home"
./gradlew --version
./gradlew tasks --all
./gradlew help --task transformText
./gradlew transformText --dry-run
--dry-run shows task execution order without executing
task actions. It is useful for graph inspection, but it does not
validate task inputs, output correctness, runtime behavior, or
incremental semantics. A “correct-looking” dry run is only one piece
of evidence.
11. Task actions are a filesystem and process trust boundary
Tasks can read files, spawn processes, access environment variables, and write arbitrary paths with the permissions of the Gradle process. Declared inputs/outputs improve Gradle’s model; they do not sandbox the task. Keep generated outputs under project build directories where practical, do not mutate source/configuration as a build side effect, and never execute untrusted task logic with production credentials.
12. DevOps operating model: the graph is an auditable contract
CI optimization depends on the graph being complete. If build selection, parallel execution, or cache reuse changes correctness, the task model is incomplete. A production-grade task should make prerequisites and state explicit enough that a clean agent can reproduce its output without depending on accidental local ordering or stale files.
Lesson 2 creates a small pipeline and makes every transition observable before introducing a typed incremental task.
Knowledge check
Does mustRunAfter(A) make task A run when only
task B is requested?
No. It is an ordering-only relationship. It affects order only when both tasks are scheduled.
Why can an undeclared file cause a stale UP-TO-DATE result?
Because the file is absent from the task state Gradle compares, so changing it may not invalidate the task.
Is FROM-CACHE the same as
UP-TO-DATE?
No. UP-TO-DATE reuses existing workspace outputs; FROM-CACHE restores outputs from an enabled build cache.
What does finalizedBy add that
mustRunAfter does not?
The finalizer is strongly associated with the finalized task and is added to the graph when that task is scheduled.
When is PathSensitivity.RELATIVE preferable to
ABSOLUTE?
When layout relative to the declared input root matters but the absolute workspace location does not.
What is the difference between up-to-date checking and an
InputChanges incremental task?
Up-to-date checking can skip the entire action; InputChanges helps a running action process only changed file inputs.
13. Bridge to the guided workflow
Next you will create a three-task file pipeline, prove its graph
with --dry-run, observe a full run and an
UP-TO-DATE repeat, invalidate one declared input, and
then implement a typed incremental transformation that reports
individual file changes.
Official references and version notes
- Gradle 9.7.1 Release Notes — pinned Gradle baseline for this chapter.
- Creating and Registering Tasks and Understanding Tasks.
- Controlling Task Execution — dependencies, ordering rules, finalizers, skip behavior, and task graph semantics.
- Incremental Build — task inputs/outputs, validation, path sensitivity, inferred dependencies, and up-to-date checks.
-
Advanced Tasks
—
InputChanges, incremental file changes,@Incremental, and normalized paths. - Implementing Custom Tasks — typed task properties and annotations.
-
Incremental Builds and Build Caching Basic
— task outcome labels such as
UP-TO-DATEandFROM-CACHE. - Build Cache — disabled by default; separate from workspace up-to-date state.
- Build Cache Concepts — repeatable outputs, path normalization, and why overlapping outputs are unsafe for reuse.
Version snapshot: Generated for August 24, 2026 with Gradle 9.7.1, JDK 21 as the Gradle runtime, and Java 17 as the course JVM target. Re-check current Gradle documentation before carrying version-sensitive behavior into future production builds.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.