# Best Practices for Tasks

version 9.0.0-branch-bambooV900HeapDump-20250829174838+0000

## Avoid DependsOn

The task dependsOn method should only be used for lifecycle tasks (tasks without task actions).

### Explanation

Tasks with actions should declare their inputs and outputs so that Gradle’s up-to-date checking can automatically determine when these tasks need to be run or rerun.

Using `dependsOn` to link tasks is a much coarser-grained mechanism that does not allow Gradle to understand why a task requires a prerequisite task to run, or which specific files from a prerequisite task are needed. `dependsOn` forces Gradle to assume that every file produced by a prerequisite task is needed by this task. This can lead to unnecessary task execution and decreased build performance.

### Example

Here is a task that writes output to two separate files:

build.gradle.kts

    abstract class SimplePrintingTask : DefaultTask() {
        @get:OutputFile
        abstract val messageFile: RegularFileProperty

        @get:OutputFile
        abstract val audienceFile: RegularFileProperty

        @TaskAction (1)
        fun run() {
            messageFile.get().asFile.writeText("Hello")
            audienceFile.get().asFile.writeText("World")
        }
    }

    tasks.register<SimplePrintingTask>("helloWorld") { (2)
        messageFile.set(layout.buildDirectory.file("message.txt"))
        audienceFile.set(layout.buildDirectory.file("audience.txt"))
    }

build.gradle

    abstract class SimplePrintingTask extends DefaultTask {
        @OutputFile
        abstract RegularFileProperty getMessageFile()

        @OutputFile
        abstract RegularFileProperty getAudienceFile()

        @TaskAction (1)
        void run() {
            messageFile.get().asFile.write("Hello")
            audienceFile.get().asFile.write("World")
        }
    }

    tasks.register("helloWorld", SimplePrintingTask) { (2)
        messageFile = layout.buildDirectory.file("message.txt")
        audienceFile = layout.buildDirectory.file("audience.txt")
    }

1 | Task With Multiple Outputs: `helloWorld` task prints "Hello" to its `messageFile` and "World" to its `audienceFile`.
2 | Registering the Task: `helloWorld` produces "message.txt" and "audience.txt" outputs.

#### Don’t Do This

If you want to translate the greeting in the `message.txt` file using another task, you could do this:

build.gradle.kts

    abstract class SimpleTranslationTask : DefaultTask() {
        @get:InputFile
        abstract val messageFile: RegularFileProperty

        @get:OutputFile
        abstract val translatedFile: RegularFileProperty

        init {
            messageFile.convention(project.layout.buildDirectory.file("message.txt"))
            translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
        }

        @TaskAction (1)
        fun run() {
            val message = messageFile.get().asFile.readText(Charsets.UTF_8)
            val translatedMessage = if (message == "Hello") "Bonjour" else "Unknown"

            logger.lifecycle("Translation: " + translatedMessage)
            translatedFile.get().asFile.writeText(translatedMessage)
        }
    }

    tasks.register<SimpleTranslationTask>("translateBad") {
        dependsOn(tasks.named("helloWorld")) (2)
    }

build.gradle

    abstract class SimpleTranslationTask extends DefaultTask {
        @InputFile
        abstract RegularFileProperty getMessageFile()

        @OutputFile
        abstract RegularFileProperty getTranslatedFile()

        SimpleTranslationTask() {
            messageFile.convention(project.layout.buildDirectory.file("message.txt"))
            translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
        }

        @TaskAction (1)
        void run() {
            def message = messageFile.get().asFile.text
            def translatedMessage = message == "Hello" ? "Bonjour" : "Unknown"

            logger.lifecycle("Translation: " + translatedMessage)
            translatedFile.get().asFile.write(translatedMessage)
        }
    }

    tasks.register("translateBad", SimpleTranslationTask) {
        dependsOn(tasks.named("helloWorld")) (2)
    }

1 | Translation Task Setup: `translateBad` requires `helloWorld` to run first to produce the message file otherwise it will fail with an error as the file does not exist.
2 | Explicit Task Dependency: Running `translateBad` will cause `helloWorld` to run first, but Gradle does not understand why.

#### Do This Instead

Instead, you should explicitly wire task inputs and outputs like this:

build.gradle.kts

    abstract class SimpleTranslationTask : DefaultTask() {
        @get:InputFile
        abstract val messageFile: RegularFileProperty

        @get:OutputFile
        abstract val translatedFile: RegularFileProperty

        init {
            messageFile.convention(project.layout.buildDirectory.file("message.txt"))
            translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
        }

        @TaskAction (1)
        fun run() {
            val message = messageFile.get().asFile.readText(Charsets.UTF_8)
            val translatedMessage = if (message == "Hello") "Bonjour" else "Unknown"

            logger.lifecycle("Translation: " + translatedMessage)
            translatedFile.get().asFile.writeText(translatedMessage)
        }
    }

    tasks.register<SimpleTranslationTask>("translateGood") {
        inputs.file(tasks.named<SimplePrintingTask>("helloWorld").map { messageFile }) (1)
    }

build.gradle

    abstract class SimpleTranslationTask extends DefaultTask {
        @InputFile
        abstract RegularFileProperty getMessageFile()

        @OutputFile
        abstract RegularFileProperty getTranslatedFile()

        SimpleTranslationTask() {
            messageFile.convention(project.layout.buildDirectory.file("message.txt"))
            translatedFile.convention(project.layout.buildDirectory.file("translated.txt"))
        }

        @TaskAction (1)
        void run() {
            def message = messageFile.get().asFile.text
            def translatedMessage = message == "Hello" ? "Bonjour" : "Unknown"

            logger.lifecycle("Translation: " + translatedMessage)
            translatedFile.get().asFile.write(translatedMessage)
        }
    }

    tasks.register("translateGood", SimpleTranslationTask) {
        inputs.file(tasks.named("helloWorld", SimplePrintingTask).map { messageFile }) (1)
    }

1 | Register Implicit Task Dependency: `translateGood` requires only one of the files that is produced by `helloWorld`.

Gradle now understands that `translateGood` requires `helloWorld` to have run successfully first because it needs to create the `message.txt` file which is then used by the translation task. Gradle can use this information to optimize task scheduling. Using the `map` method avoids eagerly retrieving the `helloWorld` task until the output is needed to determine if `translateGood` should run.

### References

- Task Inputs and Outputs

### Tags

`#tasks`, `#inputs-and-outputs`, `#up-to-date-checking`

## Favor `@CacheableTask` and `@DisableCachingByDefault` over `cacheIf(Spec)` and `doNotCacheIf(String, Spec)`

The `cacheIf` and `doNotCacheIf` methods should only be used in situations where the cacheability of a task varies between different task instances or cannot be determined until the task is executed by Gradle. You should instead favor annotating the task class itself with `@CacheableTask` annotation for any task that is always cacheable. Likewise, the `@DisableCachingByDefault` should be used to always disable caching for all instances of a task type.

### Explanation

Annotating a task type will ensure that each task instance of that type is properly understood by Gradle to be cacheable (or not cacheable). This removes the need to remember to configure each of the task instances separately in build scripts.

Using the annotations also documents the intended cacheability of the task type within its own source, appearing in Javadoc and making the task’s behavior clear to other developers without requiring them to inspect each task instance’s configuration. It is also slightly more efficient than running a test to determine cacheability.

Remember that only tasks that produce reproducible and relocatable output should be marked as `@CacheableTask`.

### Example

#### Don’t Do This

If you want to reuse the output of a task, you shouldn’t do this:

build.gradle.kts

    abstract class BadCalculatorTask : DefaultTask() { (1)
        @get:Input
        abstract val first: Property<Int>

        @get:Input
        abstract val second: Property<Int>

        @get:OutputFile
        abstract val outputFile: RegularFileProperty

        @TaskAction
        fun run() {
            val result = first.get() + second.get()
            logger.lifecycle("Result: $result")
            outputFile.get().asFile.writeText(result.toString())
        }
    }

    tasks.register<Delete>("clean") {
        delete(layout.buildDirectory)
    }

    tasks.register<BadCalculatorTask>("addBad1") {
        first = 10
        second = 25
        outputFile = layout.buildDirectory.file("badOutput.txt")
        outputs.cacheIf { true } (2)
    }

    tasks.register<BadCalculatorTask>("addBad2") { (3)
        first = 3
        second = 7
        outputFile = layout.buildDirectory.file("badOutput2.txt")
    }

build.gradle

    abstract class BadCalculatorTask extends DefaultTask {
        @Input
        abstract Property<Integer> getFirst()

        @Input
        abstract Property<Integer> getSecond()

        @OutputFile
        abstract RegularFileProperty getOutputFile()

        @TaskAction
        void run() {
            def result = first.get() + second.get()
            logger.lifecycle("Result: " + result)
            outputFile.get().asFile.write(result.toString())
        }
    }

    tasks.register("clean", Delete) {
        delete layout.buildDirectory
    }

    tasks.register("addBad1", BadCalculatorTask) {
        first = 10
        second = 25
        outputFile = layout.buildDirectory.file("badOutput.txt")
        outputs.cacheIf { true }
    }

    tasks.register("addBad2", BadCalculatorTask) {
        first = 3
        second = 7
        outputFile = layout.buildDirectory.file("badOutput2.txt")
    }

1 | Define a Task: The `BadCalculatorTask` type is deterministic and produces relocatable output, but is not annotated.
2 | Mark the Task Instance as Cacheable: This example shows how to mark a specific task instance as cacheable.
3 | Forget to Mark a Task Instance as Cacheable: Unfortunately, the `addBad2` instance of the `BadCalculatorTask` type is not marked as cacheable, so it will not be cached, despite behaving the same as `addBad1`.

#### Do This Instead

As this task meets the criteria for cacheability (we can imagine a more complex calculation in the `@TaskAction` that would benefit from automatic work avoidance via caching), you should mark the task type itself as cacheable like this:

build.gradle.kts

    @CacheableTask (1)
    abstract class GoodCalculatorTask : DefaultTask() {
        @get:Input
        abstract val first: Property<Int>

        @get:Input
        abstract val second: Property<Int>

        @get:OutputFile
        abstract val outputFile: RegularFileProperty

        @TaskAction
        fun run() {
            val result = first.get() + second.get()
            logger.lifecycle("Result: $result")
            outputFile.get().asFile.writeText(result.toString())
        }
    }

    tasks.register<Delete>("clean") {
        delete(layout.buildDirectory)
    }

    tasks.register<GoodCalculatorTask>("addGood1") { (2)
        first = 10
        second = 25
        outputFile = layout.buildDirectory.file("goodOutput.txt")
    }

    tasks.register<GoodCalculatorTask>("addGood2") {
        first = 3
        second = 7
        outputFile = layout.buildDirectory.file("goodOutput2.txt")
    }

build.gradle

    @CacheableTask (1)
    abstract class GoodCalculatorTask extends DefaultTask {
        @Input
        abstract Property<Integer> getFirst()

        @Input
        abstract Property<Integer> getSecond()

        @OutputFile
        abstract RegularFileProperty getOutputFile()

        @TaskAction
        void run() {
            def result = first.get() + second.get()
            logger.lifecycle("Result: " + result)
            outputFile.get().asFile.write(result.toString())
        }
    }

    tasks.register("clean", Delete) {
        delete layout.buildDirectory
    }

    tasks.register("addGood1", GoodCalculatorTask) {
        first = 10
        second = 25
        outputFile = layout.buildDirectory.file("goodOutput.txt")
    }

    tasks.register("addGood2", GoodCalculatorTask) { (2)
        first = 3
        second = 7
        outputFile = layout.buildDirectory.file("goodOutput2.txt")
    }

1 | Annotate the Task Type: Applying the `@CacheableTask` to a task type informs Gradle that instances of this task should always be cached.
2 | Nothing Else Needs To Be Done: When we register task instances, nothing else needs to be done - Gradle knows to cache them.

### References

- Caching Tasks

- Cacheable Tasks

- Non-cacheable Tasks

### Tags

`#tasks`, `#caching`

## Group and Describe custom Tasks

When defining custom task types or registering ad-hoc tasks, always set a clear `group` and `description`.

### Explanation

A good group name is short, lowercase, and reflects the purpose or domain of the task. For example: `documentation`, `verification`, `release`, or `publishing`.

Before creating a new group, look for an existing group name that aligns with your task’s intent. It’s often better to reuse an established category to keep the task output organized and familiar to users.

This information is used in the Tasks Report (shown via `./gradlew tasks`) to group and describe available tasks in a readable format.

Providing a group and description ensures that your tasks are:

- Displayed clearly in the report

- Categorized appropriately

- Understandable to other users (and to your future self)

| Tasks with no group are hidden from the Tasks Report unless `--all` is specified.

### Example

#### Don’t Do This

Tasks without a group appear under the "other" category in `./gradlew tasks --all` output, making them harder to locate:

app/build.gradle.kts

    tasks.register("generateDocs") {
        // Build logic to generate documentation
    }

app/build.gradle

    tasks.register('generateDocs') {
        // Build logic to generate documentation
    }

    $ gradlew :app:tasks --all

    > Task :app:tasks

    ------------------------------------------------------------
    Tasks runnable from project ':app'
    ------------------------------------------------------------

    Other tasks
    -----------
    compileJava - Compiles main Java source.
    compileTestJava - Compiles test Java source.
    generateDocs
    processResources - Processes main resources.
    processTestResources - Processes test resources.
    startScripts - Creates OS specific scripts to run the project as a JVM application.

#### Do this Instead

When defining custom tasks, always assign a clear `group` and `description`:

app/build.gradle.kts

    tasks.register("generateDocs") {
        group = "documentation"
        description = "Generates project documentation from source files."
        // Build logic to generate documentation
    }

app/build.gradle

    tasks.register('generateDocs') {
        group = 'documentation'
        description = 'Generates project documentation from source files.'
        // Build logic to generate documentation
    }

    $ gradlew :app:tasks --all

    > Task :app:tasks

    ------------------------------------------------------------
    Tasks runnable from project ':app'
    ------------------------------------------------------------

    Documentation tasks
    -------------------
    generateDocs - Generates project documentation from source files.
    javadoc - Generates Javadoc API documentation for the 'main' feature.

### References

- Task Group and Description

### Tags

`#tasks`