Chapter 17Lesson 01~180 minutes

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.

Gradle 9.7.1Task graphInputs/outputsUP-TO-DATEIncremental work

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, and finalizedBy by 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 InputChanges is 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

Task relationships are not interchangeable
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?

Why can an undeclared file cause a stale UP-TO-DATE result?

Is FROM-CACHE the same as UP-TO-DATE?

What does finalizedBy add that mustRunAfter does not?

When is PathSensitivity.RELATIVE preferable to ABSOLUTE?

What is the difference between up-to-date checking and an InputChanges incremental task?

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

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.