Gradle Tasks
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Gradle 9.8.1 and was run on JDK 26 on 2026-10-16 (the daemon ran on JDK 26; no toolchain was
involved). Every example was run through the wrapper in a scratch copy of the tidepool project, once with Kotlin DSL
build files and once with Groovy DSL build files, with --console=plain and --configuration-cache, and every working
example on this page is compatible with the configuration cache (none reported a configuration cache problem). The listings leave out blank lines, the "Calculating task graph" and "Configuration cache entry" lines and the closing
configuration cache hint to keep the focus on tasks; the two removed-syntax examples near the end of the registering section are the only builds run without the
cache, because they fail before the cache matters. The scratch copy pointed its wrapper at a
local copy of the same distribution archive; the properties file that a real project commits is
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Features newer than Gradle 8.14 name their release and say whether they are incubating, deprecated or removed. Gradle 8.14.x was not installed here, so statements about it come from the official documentation and are labelled "not run here".
A Gradle build is a graph of tasks, and everything else (plugins, dependency resolution, the build cache, the
configuration cache) exists to decide which tasks run, in which order, and whether they can be skipped. A task is the
unit of work that Gradle schedules, orders, skips and caches. In Ant the unit is the target (see
Ant Targets & Extension Points); in
Maven the unit is a plugin goal bound to a lifecycle phase (see
Maven Build Lifecycle). The shared comparison is
in Build Automation Fundamentals. This
page is about using tasks that plugins and scripts already define: listing them, registering simple ones, wiring and
ordering them, skipping them and reading their outcomes. Writing a task class with typed inputs and outputs is in
Writing Custom Tasks, how Gradle decides
that a task is up to date is in
Incremental Build, and the Property and
Provider types that connect tasks lazily are in
Lazy Configuration & Providers.
For the vocabulary (build, project, task graph) see
Gradle Core Concepts, and for when task code runs
see Build Lifecycle.
What a task is
A task is an object of type org.gradle.api.Task that belongs to a project. It has
-
a name and a path: the name is unique inside the project, the path adds the project (
:tidepool-model:jaris the taskjarof the project:tidepool-model;:stageDatais a task of the root project); -
a type: plain
Task(a task defined by a script), or a class such asCopy,Zip,Jar,JavaCompileorTestthat carries behaviour and typed properties; -
actions: Kotlin or Groovy blocks run in order when the task executes (
doFirstadds one at the front,doLastat the end), plus the single@TaskActionmethod of a task class; -
inputs and outputs, which Gradle uses to decide whether the task is up to date and whether its result can be taken from the build cache;
-
dependencies on other tasks, ordering rules and finalizers;
-
a group and a description, which only matter for display.
Gradle builds the set of tasks to run from the names on the command line. A name such as build selects the task of
that name in the current project and in every subproject; a path such as :tidepool-model:build selects one task. The
camel-case abbreviation sR for stationReport, -x to exclude a task and the other command-line rules are on
Command-Line Interface. The shared page
that sets the three tools side by side is
Comparing Ant, Maven & Gradle.
Source: Gradle User Manual: Task Basics
Listing tasks, groups and descriptions
Two tasks of the help family answer "what can I run here?": tasks lists the tasks of a project that have a group
(the groups are the headings of the listing, and a task without a group is a hidden task), and help --task <name>
describes one task. The root project of this example has two tasks in the group tidepool, a Copy task that stages the
station data and a plain task that reports how many rows were staged, and one task with no group:
val stageData = tasks.register<Copy>("stageData") {
group = "tidepool"
description = "Copies the station data into build/staging."
from("data")
into(layout.buildDirectory.dir("staging"))
}
tasks.register("stationReport") {
group = "tidepool"
description = "Prints the number of staged station rows."
dependsOn(stageData)
val staged = layout.buildDirectory.file("staging/stations.csv")
doLast {
val rows = staged.get().asFile.readLines().size - 1
println("$rows stations staged")
}
}
tasks.register("cleanupHelper") {
doLast { println("not listed: no group") }
}
def stageData = tasks.register('stageData', Copy) {
group = 'tidepool'
description = 'Copies the station data into build/staging.'
from 'data'
into layout.buildDirectory.dir('staging')
}
tasks.register('stationReport') {
group = 'tidepool'
description = 'Prints the number of staged station rows.'
dependsOn stageData
def staged = layout.buildDirectory.file('staging/stations.csv')
doLast {
def rows = staged.get().asFile.readLines().size() - 1
println "${rows} stations staged"
}
}
tasks.register('cleanupHelper') {
doLast { println 'not listed: no group' }
}
The group and description properties exist only to organise the tasks listing, so a task that people should run
gets both and a helper task that only other tasks use does not. The same output comes from both DSLs:
./gradlew --console=plain tasks --group=tidepool
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project 'tidepool'
------------------------------------------------------------
Tidepool tasks
--------------
stageData - Copies the station data into build/staging.
stationReport - Prints the number of staged station rows.
To see all tasks and more detail, run gradlew tasks --all
To see more detail about a task, run gradlew help --task <task>
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
The task cleanupHelper has no group, so it is not in that list, and it is not among the standard groups either. It is
listed under "Other tasks" when tasks --all is used, which also shows every task without a group and the tasks of
subprojects (the middle part of the output, the standard Build Setup and Help groups, is left out):
./gradlew --console=plain tasks --all
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project 'tidepool'
------------------------------------------------------------
...
Tidepool tasks
--------------
stageData - Copies the station data into build/staging.
stationReport - Prints the number of staged station rows.
Other tasks
-----------
cleanupHelper
prepareKotlinBuildScriptModel
BUILD SUCCESSFUL in 717ms
1 actionable task: 1 executed
prepareKotlinBuildScriptModel is an internal task that the IDE integration uses; it is listed in both DSLs, and
kotlinDslAccessorsReport, a Help task of the Kotlin DSL support, was listed only in the Kotlin DSL run. A hidden task
is still a normal task; it just is not advertised:
./gradlew --console=plain cleanupHelper
> Task :cleanupHelper
not listed: no group
BUILD SUCCESSFUL in 774ms
1 actionable task: 1 executed
help --task prints everything Gradle knows about a task: its path with the place where it was registered (see
Task provenance), its type, the command-line options that its class declares (every task has --rerun, which runs
it even if it is up to date), the description and the group. The Groovy DSL run printed the same text with
build.gradle in the path line:
./gradlew --console=plain help --task stationReport
> Task :help
Detailed task information for stationReport
Path
:stationReport (registered in build file 'build.gradle.kts')
Type
Task (org.gradle.api.Task)
Options
--rerun Causes the task to be re-run even if up-to-date.
Description
Prints the number of staged station rows.
Group
tidepool
BUILD SUCCESSFUL in 730ms
1 actionable task: 1 executed
Names can be abbreviated to the letters of their camel-case humps as long as the result is unambiguous. The abbreviation
sR selected stationReport; the command line page covers the rule and its errors:
./gradlew --console=plain sR
> Task :stageData UP-TO-DATE
> Task :stationReport
3 stations staged
BUILD SUCCESSFUL in 877ms
2 actionable tasks: 1 executed, 1 up-to-date
Because stationReport calls dependsOn(stageData), stageData ran first (it was up to date here because an earlier
command had already staged the files). Task groups and descriptions are the documentation of your build for the people
who run it; the best practices page recommends that every task that is meant to be run has both.
The task graph of a build
A lifecycle task has no actions of its own; it exists to give a name to a set of other tasks. The tasks of the Java
plugins that every JVM build runs are a good example: build is a lifecycle task that depends on assemble (everything
that produces outputs) and check (everything that verifies them), and each of those fans out to the tasks that do the
work. This is the graph that ./gradlew :tidepool-model:build runs, with the dependencies that Gradle itself reported
for each task (read from the task graph in a throw-away copy of the build):
Gradle runs the tasks in an order that respects every arrow, running a task at most once. --dry-run (-m) prints that
order without executing anything; it is the quickest way to answer "what will this command do?". For the application
module, which depends on the other two, the dry run lists the compile tasks of all three modules before the packaging
tasks of the application (the order of tasks that do not depend on each other is not specified and may differ between
runs):
./gradlew --console=plain :tidepool-cli:build --dry-run
:tidepool-model:compileJava SKIPPED
:tidepool-engine:compileJava SKIPPED
:tidepool-cli:compileJava SKIPPED
:tidepool-cli:processResources SKIPPED
:tidepool-cli:classes SKIPPED
:tidepool-cli:jar SKIPPED
:tidepool-engine:processResources SKIPPED
:tidepool-engine:classes SKIPPED
:tidepool-engine:jar SKIPPED
:tidepool-model:processResources SKIPPED
:tidepool-model:classes SKIPPED
:tidepool-model:jar SKIPPED
:tidepool-cli:startScripts SKIPPED
:tidepool-cli:distTar SKIPPED
:tidepool-cli:distZip SKIPPED
:tidepool-cli:assemble SKIPPED
:tidepool-cli:compileTestJava SKIPPED
:tidepool-cli:processTestResources SKIPPED
:tidepool-cli:testClasses SKIPPED
:tidepool-cli:test SKIPPED
:tidepool-cli:check SKIPPED
:tidepool-cli:build SKIPPED
BUILD SUCCESSFUL in 811ms
Every task shows as SKIPPED in a dry run, because none of them executed; this is the only meaning of the label in that
output. The tasks of other modules appear because the application’s compileJava needs the jars (or classes) of its
project dependencies.
Task outcomes
When a task finishes, Gradle labels it in the console with one of five outcomes. The label tells you why the task did or did not do work, and reading the labels is the fastest way to see whether the incremental build and the build cache work:
| Label | Meaning | Typical cause |
|---|---|---|
none, or |
The task ran its actions. |
The task has actions and Gradle executed them; a lifecycle task shows no label when one of its dependencies executed. |
|
The task’s outputs are still valid, so nothing ran. |
Inputs and outputs did not change since the last run (see
Incremental Build); a lifecycle task whose dependencies are all |
|
The outputs were restored from the build cache instead of running the task. |
The build cache holds an entry for the same inputs, for example after |
|
The task did not run, although it was selected. |
An |
|
The task has inputs and outputs but no source files, so it had nothing to do. |
|
The labels come from real runs of the tidepool-model module, which has no resources and no tests. From a clean
checkout the first build executes the compile and packaging tasks and gives NO-SOURCE to the tasks that have no
inputs. The first run is cold, so compileJava and jar are executed:
./gradlew --console=plain --configuration-cache :tidepool-model:build
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-model:processTestResources NO-SOURCE
> Task :tidepool-model:compileJava
> Task :tidepool-model:classes
> Task :tidepool-model:compileTestJava NO-SOURCE
> Task :tidepool-model:testClasses UP-TO-DATE
> Task :tidepool-model:test NO-SOURCE
> Task :tidepool-model:check UP-TO-DATE
> Task :tidepool-model:jar
> Task :tidepool-model:assemble
> Task :tidepool-model:build
BUILD SUCCESSFUL in 847ms
2 actionable tasks: 2 executed
Running the same command again changes nothing, so every task that has outputs is UP-TO-DATE; the tasks without source
files are still NO-SOURCE, and check, testClasses and classes are lifecycle tasks that report UP-TO-DATE
because everything they depend on is:
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-model:processTestResources NO-SOURCE
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-model:classes UP-TO-DATE
> Task :tidepool-model:jar UP-TO-DATE
> Task :tidepool-model:assemble UP-TO-DATE
> Task :tidepool-model:compileTestJava NO-SOURCE
> Task :tidepool-model:testClasses UP-TO-DATE
> Task :tidepool-model:test NO-SOURCE
> Task :tidepool-model:check UP-TO-DATE
> Task :tidepool-model:build UP-TO-DATE
BUILD SUCCESSFUL in 684ms
2 actionable tasks: 2 up-to-date
With the build cache on (--build-cache), clean followed by build restores the compiled classes instead of
compiling them again. compileJava shows FROM-CACHE; the jar task is executed again, because archive tasks such as
Jar are not cacheable by default. The cache already held an entry for the same sources from an earlier run, which is why
even the first cached run after a clean restores compileJava, and the closing summary counts the outcomes:
./gradlew --console=plain --configuration-cache --build-cache :tidepool-model:clean :tidepool-model:build
> Task :tidepool-model:clean
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-model:processTestResources NO-SOURCE
> Task :tidepool-model:compileJava FROM-CACHE
> Task :tidepool-model:classes UP-TO-DATE
> Task :tidepool-model:compileTestJava NO-SOURCE
> Task :tidepool-model:testClasses UP-TO-DATE
> Task :tidepool-model:test NO-SOURCE
> Task :tidepool-model:check UP-TO-DATE
> Task :tidepool-model:jar
> Task :tidepool-model:assemble
> Task :tidepool-model:build
BUILD SUCCESSFUL in 720ms
3 actionable tasks: 2 executed, 1 from cache
The Groovy DSL builds printed the same labels for the same tasks (only the order of the lines for tasks without a dependency between them differed). The summary line "3 actionable tasks: 2 executed, 1 from cache" is the sum of all labels; an actionable task is one that has actions, so lifecycle tasks are not counted.
To see why a task was not executed rather than only that it was not, run with --info. The next example is from the
onlyIf section below; the two interesting lines of a long log (most of the log is daemon and cache bookkeeping, left
out here with the marked ellipses) are
...
> Task :upload SKIPPED
Skipping task ':upload' as task onlyIf 'the 'offline' property is not set' is false.
...
For the cache and the incremental build see Build Cache and Incremental Build; the cache keys themselves are explained in the build cache page.
Lifecycle tasks and actionable tasks
An actionable task has actions (a copy, a compile, a script block) and usually inputs and outputs. A lifecycle task
has no actions: it only collects other tasks under one name, so that ./gradlew build means "everything that makes the
project buildable". The Java and base plugins define assemble, check and build for that purpose, and your own build
should follow the same pattern: give people a few lifecycle tasks with clear names (dataset, qualityCheck) and put the
work in actionable tasks that the lifecycle tasks depend on. dependsOn is the right tool only for this: a lifecycle
task has nothing to wire to, so it can only name tasks.
In this example stageData copies the station files, packData zips what stageData produced, and dataset is the
lifecycle task that people run:
val stageData = tasks.register<Copy>("stageData") {
from("data")
into(layout.buildDirectory.dir("staging"))
}
// Wired input: packData consumes the outputs of stageData, so Gradle adds the
// dependency by itself and knows which files are inputs.
val packData = tasks.register<Zip>("packData") {
from(stageData)
archiveFileName = "stations.zip"
destinationDirectory = layout.buildDirectory.dir("dist")
}
// Lifecycle task: no actions, only dependencies.
tasks.register("dataset") {
group = "tidepool"
description = "Builds the station data package."
dependsOn(packData)
}
def stageData = tasks.register('stageData', Copy) {
from 'data'
into layout.buildDirectory.dir('staging')
}
// Wired input: packData consumes the outputs of stageData, so Gradle adds the
// dependency by itself and knows which files are inputs.
def packData = tasks.register('packData', Zip) {
from stageData
archiveFileName = 'stations.zip'
destinationDirectory = layout.buildDirectory.dir('dist')
}
// Lifecycle task: no actions, only dependencies.
tasks.register('dataset') {
group = 'tidepool'
description = 'Builds the station data package.'
dependsOn packData
}
Note what is missing: packData does not use dependsOn. Its from(stageData) passes the task provider as a source,
and Gradle understands that the files to zip are the outputs of stageData, so it adds the dependency itself; this is
called wiring an input to an output. The dry run shows the order, and the first real run executes all three tasks. The
second run finds everything up to date, including the lifecycle task:
./gradlew --console=plain --dry-run packData
:stageData SKIPPED
:packData SKIPPED
BUILD SUCCESSFUL in 684ms
./gradlew --console=plain dataset
> Task :stageData
> Task :packData
> Task :dataset
BUILD SUCCESSFUL in 723ms
2 actionable tasks: 2 executed
> Task :stageData UP-TO-DATE
> Task :packData UP-TO-DATE
> Task :dataset UP-TO-DATE
BUILD SUCCESSFUL in 662ms
2 actionable tasks: 2 up-to-date
The Groovy DSL run printed the same lines. The "2 actionable tasks" count excludes dataset, because it has no
actions.
Registering and configuring tasks
There are two generations of the API that creates tasks, and the difference decides how long the configuration phase takes in a large build.
-
The eager API (
tasks.create(...), the Groovytask name { }shorthand,tasks.getByName(...)) creates the task and runs its configuration block immediately, while the build script is evaluated, for every task, whether or not the command line needs it. -
The lazy API (
tasks.register(...),tasks.named(...),configureEach) records a recipe, returns aTaskProviderand creates and configures the task only when something needs the task: the command line selected it, another task depends on it, or code calledget()on the provider. This is called task configuration avoidance. It arrived as an incubating API in Gradle 4.9 (2018), and the Gradle documentation has recommended it ever since.
This build registers one task the eager way and two the lazy way, and prints a line from every configuration block, so that the output shows when each block ran:
// Eager: the configuration block runs while the script is evaluated.
tasks.create("eagerReport") {
println("configuring eagerReport")
doLast { println("running eagerReport") }
}
// Lazy: the block runs only if something asks for the task.
tasks.register("lazyReport") {
println("configuring lazyReport")
doLast { println("running lazyReport") }
}
// Lazy configuration of every task of a type, without realizing unused tasks.
tasks.withType<Copy>().configureEach {
println("configuring Copy task $name")
}
tasks.register<Copy>("stageData") {
from("data")
into(layout.buildDirectory.dir("staging"))
}
tasks.register<Copy>("stageBackup") {
from("data")
into(layout.buildDirectory.dir("backup"))
}
// Configure an existing task lazily, by name and type.
tasks.named<Copy>("stageBackup") {
description = "Copies the station data to build/backup."
}
// Eager: the configuration block runs while the script is evaluated.
tasks.create('eagerReport') {
println 'configuring eagerReport'
doLast { println 'running eagerReport' }
}
// Lazy: the block runs only if something asks for the task.
tasks.register('lazyReport') {
println 'configuring lazyReport'
doLast { println 'running lazyReport' }
}
// Lazy configuration of every task of a type, without realizing unused tasks.
tasks.withType(Copy).configureEach {
println "configuring Copy task ${name}"
}
tasks.register('stageData', Copy) {
from 'data'
into layout.buildDirectory.dir('staging')
}
tasks.register('stageBackup', Copy) {
from 'data'
into layout.buildDirectory.dir('backup')
}
// Configure an existing task lazily, by name and type.
tasks.named('stageBackup', Copy) {
description = 'Copies the station data to build/backup.'
}
Running help needs none of these tasks, yet the eager task is configured. The Kotlin DSL also warns at compile time
that create is deprecated (the path prefix in the warning is replaced by a neutral one); the Groovy DSL does not warn.
Output of the Kotlin DSL run:
./gradlew --console=plain --configuration-cache help
> Configure project :
w: file:///home/dev/tidepool/build.gradle.kts:2:7: 'fun create(name: String, configuration: Action<in Task>): Task' is deprecated. Deprecated in Java.
configuring eagerReport
> Task :help
Welcome to Gradle 9.8.1.
To run a build, run gradlew <task> ...
To see a list of available tasks, run gradlew tasks
To see more detail about a task, run gradlew help --task <task>
To see a list of command-line options, run gradlew --help
For more detail on using Gradle, see https://docs.gradle.org/9.8.1/userguide/command_line_interface.html
For troubleshooting, visit https://help.gradle.org
BUILD SUCCESSFUL in 949ms
1 actionable task: 1 executed
Only eagerReport was configured. Asking for lazyReport configures it, and nothing else but the eager task; asking for
stageData runs the configureEach block for the one Copy task that exists in the graph, and not for stageBackup,
which stays an unrealized recipe:
./gradlew --console=plain lazyReport
./gradlew --console=plain stageData
> Configure project :
configuring eagerReport
configuring lazyReport
> Task :lazyReport
running lazyReport
BUILD SUCCESSFUL in 608ms
1 actionable task: 1 executed
> Configure project :
configuring eagerReport
configuring Copy task stageData
> Task :stageData
BUILD SUCCESSFUL in 628ms
1 actionable task: 1 executed
Asking for help --task stageBackup is a request for that task, so its recipe runs, including the named block that set
the description:
> Configure project :
configuring eagerReport
configuring Copy task stageBackup
> Task :help
Detailed task information for stageBackup
Path
:stageBackup (registered in build file 'build.gradle.kts')
Type
Copy (org.gradle.api.tasks.Copy)
Options
--rerun Causes the task to be re-run even if up-to-date.
Description
Copies the station data to build/backup.
Group
-
BUILD SUCCESSFUL in 604ms
1 actionable task: 1 executed
The Groovy DSL printed the same lines for every command (the path line says build.gradle instead of
build.gradle.kts), apart from the compile warning. The shape to remember is
| To… | Use (lazy) | Avoid (eager) |
|---|---|---|
create a task |
|
|
configure one task by name |
|
|
configure all tasks of a type |
|
|
keep a reference |
the |
looking the task up again by name just to get an object |
depend on a task |
pass the provider or the name: |
calling |
Two further rules from the migration guide are worth keeping. A configuration block of the lazy API should only change
the task it belongs to (not other tasks or the project), because it may run at any time or never. And an iteration over
the whole task collection (a for loop, forEach, findAll) or remove forces every task into existence, so avoid
them in build logic that has to stay fast.
Source: Gradle User Manual: Task Configuration Avoidance, How do I defer task creation?, Old versus new configuration API, Eager APIs to avoid, Defining tasks, Configuring tasks, Gradle 4.9 release notes
Removed task syntax
Two spellings from older builds no longer compile. Both were run on 9.8.1.
The first is the left-shift form task name << { ... } that was the standard way to add an action in the first Gradle
releases. It was removed in Gradle 5.0 (the upgrade guide for 4.x lists it among the changes that arrive as
deprecation warnings in 4.10 and stop working in 5.0); doLast replaces it. The Groovy DSL build file
task hello << {
println 'Hello from a Gradle 1.x task'
}
fails during configuration, and the fix is tasks.register('hello') { doLast { ... } }. This was run without the
configuration cache, because the build fails before the cache matters:
* Where:
Build file '/home/dev/tidepool/build.gradle' line: 1
* What went wrong:
A problem occurred evaluating root project 'tidepool'.
> Could not find method leftShift() for arguments [build_2eo2j9lhbmxnm2dhmoext2o6v$_run_closure1@40c38b20] on task ':hello' of type org.gradle.api.DefaultTask.
The second is the Kotlin DSL notation "name"() that referenced a task by a string. It was deprecated in
the 8.x line (the upgrade guide for 8.x announces its removal in 9.0.0) and removed in Gradle 9.0.0; use named("name"). Inside a tasks { } block, the line "hello"() now gives a script
compilation error (this was run once without the configuration cache, with the path prefix of the build file replaced):
e: file:///home/dev/tidepool/build.gradle.kts:4:12: No value passed for parameter 'configuration'.
FAILURE: Build failed with an exception.
* Where:
Build file '/home/dev/tidepool/build.gradle.kts' line: 4
* What went wrong:
Script compilation error:
Line 4: "hello"()
^ No value passed for parameter 'configuration'.
1 error
The related form "name" { ... } inside a tasks { } block, which configures a task of that name, is not part of
the removal and still works in 9.8.1. The Groovy DSL and the Groovy task name { } shorthand were not affected; the
shorthand still creates a task eagerly, so it is better to switch it to tasks.register.
Dependencies and ordering
Four methods connect tasks, and they answer two different questions. Dependency methods decide what runs; ordering methods decide in which order, if both run anyway.
| Method | Meaning | Adds the other task to the build? |
|---|---|---|
|
|
Yes. |
|
If both tasks are in the build, this task runs after |
No. Running this task alone does not run |
|
The same, but only a preference: Gradle ignores it when honouring it would create a cycle, and in a parallel build when every other dependency of the task is satisfied and only the "should run after" task is still pending. |
No. |
|
|
Yes (the finalizer). |
All of them accept a task provider, a task name, or anything else that dependsOn accepts. The next build has four
tasks. publish needs fetch, must run after validate when both are requested, and is finalized by notify.
Two Gradle properties switch on a failing publish and a circular dependency, so that the same file shows the error
cases:
fun step(name: String) = tasks.register(name) {
doLast { println("running $name") }
}
val fetch = step("fetch")
val validate = step("validate")
val publish = step("publish")
val notify = step("notify")
publish {
dependsOn(fetch) // publish needs fetch, so fetch joins the graph
mustRunAfter(validate) // if validate is also requested, it goes first
finalizedBy(notify) // notify runs after publish, even if publish fails
}
if (providers.gradleProperty("failPublish").isPresent) {
publish { doLast { throw GradleException("publish failed on purpose") } }
}
if (providers.gradleProperty("cycle").isPresent) {
fetch { dependsOn(publish) }
}
def step(String name) {
tasks.register(name) {
doLast { println "running ${name}" }
}
}
def fetch = step('fetch')
def validate = step('validate')
def publish = step('publish')
def notify = step('notify')
publish.configure {
dependsOn fetch // publish needs fetch, so fetch joins the graph
mustRunAfter validate // if validate is also requested, it goes first
finalizedBy notify // notify runs after publish, even if publish fails
}
if (providers.gradleProperty('failPublish').present) {
publish.configure { doLast { throw new GradleException('publish failed on purpose') } }
}
if (providers.gradleProperty('cycle').present) {
fetch.configure { dependsOn publish }
}
Running publish alone pulls fetch in through dependsOn, and adds notify through finalizedBy; validate is not
part of the build, because mustRunAfter does not add anything:
./gradlew --console=plain publish
> Task :fetch
running fetch
> Task :publish
running publish
> Task :notify
running notify
BUILD SUCCESSFUL in 802ms
3 actionable tasks: 3 executed
When validate is requested as well, it runs before publish, whatever the order on the command line (the next output is
from publish validate; validate publish gave the same). Nothing orders fetch and validate against each other, so
Gradle is free to run them in either order; in the Kotlin DSL run above fetch ran first and in the Groovy DSL run
validate did. Never depend on the order of two tasks that no rule connects:
./gradlew --console=plain publish validate
> Task :fetch
running fetch
> Task :validate
running validate
> Task :publish
running publish
> Task :notify
running notify
BUILD SUCCESSFUL in 835ms
4 actionable tasks: 4 executed
A finalizer runs even when the finalized task fails, which is what makes it suitable for cleanup (stopping a server,
deleting a temporary directory). With -PfailPublish, publish throws; notify still runs, and the failure message
names the task and, since Gradle 9.5, where it was registered (see Task provenance):
./gradlew --console=plain publish -PfailPublish
> Task :fetch
running fetch
> Task :publish FAILED
running publish
> Task :notify
running notify
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':publish' (registered in build file 'build.gradle.kts').
> publish failed on purpose
BUILD FAILED in 798ms
3 actionable tasks: 3 executed
The Groovy DSL run printed the same lines, with a Where: line in front that names the build file and the line of the
failing action. Finally, -Pcycle makes fetch depend on publish, which is a cycle. Gradle refuses to run anything and
prints the cycle; the error occurs while the task graph is calculated, so there is no configuration to cache:
./gradlew --console=plain publish -Pcycle
FAILURE: Build failed with an exception.
* What went wrong:
Circular dependency between the following tasks:
:fetch
\--- :publish
\--- :fetch (*)
(*) - details omitted (listed previously)
BUILD FAILED in 903ms
The difference between mustRunAfter and shouldRunAfter shows in a two-task build where each task should run after
the other, which is an ordering cycle. The shouldRunAfter pair is accepted and Gradle silently drops one rule; the
mustRunAfter pair is the same circular dependency error as above:
val a = tasks.register("a") { doLast { println("running a") } }
val b = tasks.register("b") { doLast { println("running b") } }
val strict = providers.gradleProperty("strict").isPresent
a {
if (strict) mustRunAfter(b) else shouldRunAfter(b)
}
b {
if (strict) mustRunAfter(a) else shouldRunAfter(a)
}
def a = tasks.register('a') { doLast { println 'running a' } }
def b = tasks.register('b') { doLast { println 'running b' } }
def strict = providers.gradleProperty('strict').present
a.configure {
if (strict) { mustRunAfter b } else { shouldRunAfter b }
}
b.configure {
if (strict) { mustRunAfter a } else { shouldRunAfter a }
}
./gradlew --console=plain a b
> Task :a
running a
> Task :b
running b
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
> Task :b
running b
> Task :a
running a
BUILD SUCCESSFUL in 710ms
2 actionable tasks: 2 executed
Which of the two rules is dropped is not specified: the Kotlin DSL run executed a then b, the Groovy DSL run b then
a. With -Pstrict both DSLs fail with the cycle error:
FAILURE: Build failed with an exception.
* What went wrong:
Circular dependency between the following tasks:
:a
\--- :b
\--- :a (*)
(*) - details omitted (listed previously)
BUILD FAILED in 901ms
Wire inputs instead of using dependsOn
dependsOn between two tasks with actions is the coarsest way to express a relationship: it says "this task needs that task
to have run" and nothing about which files it needs. Gradle must then assume that every file the first task produces is
an input of the second, which can cause tasks to rerun when an unrelated output changes. The best practices guide
therefore reserves dependsOn for lifecycle tasks (as in the dataset task above) and recommends connecting actionable
tasks through inputs and outputs, as packData did with from(stageData). With a custom task class this means
declaring an @InputFile property and setting it from the output property of the producer; Gradle derives the
dependency and the up-to-date check from that wiring (see
Writing Custom Tasks and
Lazy Configuration & Providers).
A related guideline says not to hard-code task names unless they are documented as public API, because most task names of
Gradle and of third-party plugins are internal details that can be renamed or removed; prefer a plugin’s DSL, then task
types, then names. Passing a task provider, as from(stageData) does, avoids a name altogether.
In the build of "Listing tasks, groups and descriptions" above, stationReport used dependsOn(stageData) on purpose, because it is a simple script task that
reads a file in doLast and has no declared inputs. In a real build the report would be a task class whose input is the
staged directory.
Skipping and limiting tasks
Gradle offers several ways to keep a selected task from doing its work. They differ in what the build log shows and in whether dependants are affected.
| Mechanism | Effect | Log label |
|---|---|---|
|
The predicate is evaluated just before execution; false skips the task’s actions. It can carry a reason string
( |
|
|
The task never runs its actions. |
|
|
Thrown from an action, it stops that action and the following ones of the task; the build goes on with the next task. |
none (the task counts as executed) |
|
Removes the named task from the build; tasks that only it needed also drop out of the log (see below). The user guide calls this a practice to avoid and recommends a suitable lifecycle task instead. |
The task does not appear in the log. |
|
Interrupts the task when it runs too long and fails it. |
|
This build has one example of each (the Gradle properties let the same file show the skipped and the executed cases):
import java.time.Duration
val upload = tasks.register("upload") {
doLast { println("uploading") }
}
upload {
val offline = providers.gradleProperty("offline")
onlyIf("the 'offline' property is not set") { !offline.isPresent }
}
tasks.register("legacyExport") {
enabled = false
doLast { println("never printed") }
}
tasks.register("guarded") {
doFirst {
throw StopExecutionException("nothing to do")
}
doLast { println("never printed either") }
}
tasks.register("slow") {
timeout = Duration.ofMillis(500)
doLast { Thread.sleep(60_000) }
}
tasks.register("afterSlow") {
mustRunAfter("slow")
doLast { println("afterSlow ran") }
}
import java.time.Duration
def upload = tasks.register('upload') {
doLast { println 'uploading' }
}
upload.configure {
def offline = providers.gradleProperty('offline')
onlyIf("the 'offline' property is not set") { !offline.present }
}
tasks.register('legacyExport') {
enabled = false
doLast { println 'never printed' }
}
tasks.register('guarded') {
doFirst {
throw new StopExecutionException('nothing to do')
}
doLast { println 'never printed either' }
}
tasks.register('slow') {
timeout = Duration.ofMillis(500)
doLast { Thread.sleep(60_000) }
}
tasks.register('afterSlow') {
mustRunAfter 'slow'
doLast { println 'afterSlow ran' }
}
With no property upload runs. With -Poffline the predicate is false and the task is SKIPPED, and --info prints
the reason; legacyExport is enabled = false, so it is always SKIPPED:
./gradlew --console=plain upload
./gradlew --console=plain upload -Poffline
./gradlew --console=plain legacyExport
> Task :upload
uploading
BUILD SUCCESSFUL in 825ms
1 actionable task: 1 executed
> Task :upload SKIPPED
BUILD SUCCESSFUL in 770ms
> Task :legacyExport SKIPPED
BUILD SUCCESSFUL in 1s
The onlyIf predicate reads the property through a Provider (providers.gradleProperty(...)) that is captured outside
the lambda, so the predicate does not touch the project object when it runs; that is what keeps it compatible with the
configuration cache. The Groovy script does the same.
guarded throws StopExecutionException in its doFirst, so its doLast never runs, yet the build carries on and
afterSlow runs; the task is shown without a label, as executed:
./gradlew --console=plain guarded afterSlow
> Task :guarded
> Task :afterSlow
afterSlow ran
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
A timeout interrupts the thread that runs the task, so it works for built-in tasks and for any code that responds to
interrupts (Thread.sleep does). The task is FAILED and finalizers still run. Without --continue the rest of the
build is abandoned; with --continue, tasks that do not depend on the failed one carry on. This build was run once
for each form:
./gradlew --console=plain slow afterSlow
./gradlew --console=plain --continue slow afterSlow
> Task :slow FAILED
Requesting stop of task ':slow' as it has exceeded its configured timeout of 500ms.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':slow' (registered in build file 'build.gradle.kts').
> Timeout has been exceeded
BUILD FAILED in 1s
1 actionable task: 1 executed
> Task :slow FAILED
Requesting stop of task ':slow' as it has exceeded its configured timeout of 500ms.
> Task :afterSlow
afterSlow ran
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':slow' (registered in build file 'build.gradle.kts').
> Timeout has been exceeded
BUILD FAILED in 1s
2 actionable tasks: 2 executed
Only the first run’s output is shown in the Kotlin DSL form; the Groovy DSL printed the same lines. An unresponsive task, for example a long CPU loop without interruption checks, cannot be timed out.
To leave out a task from the command line use -x (or --exclude-task). Excluding a task also excludes
dependencies that only that task needed. With stationReport depending on stageData:
./gradlew --console=plain stationReport -x stageData
> Task :stationReport
3 stations staged
BUILD SUCCESSFUL in 873ms
1 actionable task: 1 executed
stageData does not appear at all, and stationReport ran against the files left by an earlier run. Excluding a task
also removes the tasks that only it needed: in the tidepool-model module, build -x test (run with the same options)
did not list test and also did not list compileTestJava, testClasses or the test resource task:
> Task :tidepool-model:check UP-TO-DATE
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-model:classes UP-TO-DATE
> Task :tidepool-model:jar UP-TO-DATE
> Task :tidepool-model:assemble UP-TO-DATE
> Task :tidepool-model:build UP-TO-DATE
BUILD SUCCESSFUL in 6s
2 actionable tasks: 2 up-to-date
Excluding a task is a way to speed up a local build, and it is dangerous when the excluded task produces something the others need; CI should not use it to hide failures.
Task provenance
Since Gradle 9.5 (2026-04-28) Gradle records where each task was registered: in a build script, in the settings script, or
by a plugin. The information appears in failure messages, in help --task, and in tasks --provenance. The failure
messages above already showed it for script-registered tasks:
Execution failed for task ':publish' (registered in build file 'build.gradle.kts').
For a task registered by a plugin the text names the plugin class. A compilation error in tidepool-model (a
deliberately broken source file added to a scratch copy) gave
Execution failed for task ':tidepool-model:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
tasks --provenance adds the same information to the listing, which answers the common question "which plugin gave me this
task?":
./gradlew --console=plain :tidepool-model:tasks --provenance --group=build
> Task :tidepool-model:tasks
------------------------------------------------------------
Tasks runnable from project ':tidepool-model'
------------------------------------------------------------
Build tasks
-----------
assemble (registered by plugin class 'org.gradle.language.base.plugins.LifecycleBasePlugin') - Assembles the outputs of this project.
build (registered by plugin class 'org.gradle.language.base.plugins.LifecycleBasePlugin') - Assembles and tests this project.
buildDependents (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin') - Assembles and tests this project and all projects that depend on it.
buildNeeded (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin') - Assembles and tests this project and all projects it depends on.
classes (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin') - Assembles main classes.
clean (registered by plugin class 'org.gradle.language.base.plugins.LifecycleBasePlugin') - Deletes the build directory.
jar (registered by plugin class 'org.gradle.api.plugins.JavaPlugin') - Assembles a jar archive containing the classes of the 'main' feature.
testClasses (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin') - Assembles test classes.
To see all tasks and more detail, run gradlew tasks --all
To see more detail about a task, run gradlew help --task <task>
BUILD SUCCESSFUL in 767ms
1 actionable task: 1 executed
help --task jar names the plugin for each project that has the task; the Paths heading appears because three
projects have a jar task:
> Task :tidepool-model:help
Detailed task information for jar
Paths
:tidepool-cli:jar (registered by plugin class 'org.gradle.api.plugins.JavaPlugin')
:tidepool-engine:jar (registered by plugin class 'org.gradle.api.plugins.JavaPlugin')
:tidepool-model:jar (registered by plugin class 'org.gradle.api.plugins.JavaPlugin')
Type
Jar (org.gradle.api.tasks.bundling.Jar)
Options
--rerun Causes the task to be re-run even if up-to-date.
Description
Assembles a jar archive containing the classes of the 'main' feature.
Group
build
BUILD SUCCESSFUL in 816ms
1 actionable task: 1 executed
Provenance is omitted from verification failures such as test failures, because those are expected outcomes and not configuration problems. The same build on Gradle 8.14 does not print provenance (it is new in 9.5; not run here).
Gradle 8.14 and Gradle 9
The task model is stable across the two lines. The differences that matter when a build moves from 8.14 to 9.8.1 are (the 8.14 column is from the official documentation and release notes, not run here):
| Topic | Gradle 8.14 | Gradle 9.8.1 |
|---|---|---|
|
Deprecated since the 8.x line; works with a warning (documentation). |
Removed in 9.0.0; use |
|
Eager API, recommended against, and deprecated in the |
Still available; the Kotlin DSL prints a deprecation warning at compile time (run above). |
Task provenance |
Not available. |
Failure messages, |
|
Deprecated since 8.11. |
Removed in 9.0.0; use an injected |
|
Deprecated, and the configuration cache reports it. |
Same, and the configuration cache is the preferred mode in 9.x (see Configuration Cache & Isolated Projects). |
|
Only the plain |
A lazy |
Equivalents in Ant and Maven
| Gradle | Maven | Ant |
|---|---|---|
Task (an object with inputs, outputs and actions) |
Plugin goal ( |
Task element inside a target; the unit that is scheduled is the target, see Ant Targets & Extension Points. |
Lifecycle task ( |
Lifecycle phase ( |
A target with only a |
|
Implicit: a phase runs all earlier phases; goals run in the order of the bindings. |
The |
|
No equivalent; the phase order is fixed. |
No equivalent; the order is the order of |
|
No equivalent in the lifecycle; a failing goal stops the build ( |
No equivalent in core Ant; the third-party Ant-Contrib |
|
|
The |
|
|
No general equivalent. |
|
No general per-goal up-to-date check in core Maven; the Apache Maven Build Cache Extension adds caching (see Build Performance: Cache, Parallel Builds & mvnd). |
The |
Where the books are outdated
Gradle in Action (Muschko, Manning 2014) was written for Gradle 1.7. Its model of tasks (a task is an object with actions, dependencies and a type) is still right, but some of what Gradle 1.7 allowed no longer works or is no longer advised:
-
task name << { ... }. The left-shift operator was the short way to add an action in the Gradle 1.x era. It was removed in Gradle 5.0; usedoLast { ... }or, better,tasks.register("name") { doLast { ... } }(run above). -
Eager task creation. The only task API in Gradle 1.7 was eager (
task name(type: Copy),tasks.create,tasks.getByName). The lazytasks.registerandtasks.namedarrived in Gradle 4.9 (2018) and are the recommended API now. -
dependsOnas the main wiring tool. Prefer inputs and outputs wired through properties;dependsOnstays right for lifecycle tasks. -
The Kotlin DSL. Builds from that era are Groovy only. The Kotlin DSL has been stable since Gradle 5.0 and is the default for
gradle initsince 8.2, and its"name"()notation is gone (run above).
For the same topics in the other tools see Maven Build Lifecycle and Ant Targets & Extension Points; for the next steps in Gradle see Writing Custom Tasks and Incremental Build.