Declaring Gradle task inputs and outputs is essential for your build to work properly. By telling Gradle what files or properties your task consumes and produces, the incremental build feature kicks in, improving the performance of your build. You’ll also be following best practices, allowing you to easily create more complex projects.

In this article, you’ll learn how Gradle task inputs and outputs work, why they’re important, as well as practically how to implement them in your own projects.

Tasks 30,000 foot view

A task represents a unit of work to get done in your Gradle project. That could be compiling code, copying files, publishing an artifact, or whatever action your task needs to accomplish.

You run tasks on the command line. For example, running ./gradlew compileJava compiles your project’s .java files into .class files.

Task inputs and outputs

For a task to actually do anything useful, it needs some stuff to work on. This is called the task inputs.

Your task normally produces something. This is called the task outputs.

Overview of task inputs and outputs

Overview of task inputs and outputs

Most official Gradle tasks have inputs and outputs. Can you guess what the inputs and outputs are for the compileJava task?

It’s actually quite straightforward. The inputs of compileJava are the source .java files and the Java version, and the outputs are the compiled .class files.

Task inputs and outputs for the compileJava task

Task inputs and outputs for the compileJava task

The relationship between task inputs and outputs is normally that a change in the inputs creates a change in the outputs. In the case of compileJava, if we change the .java files, it makes sense that the compiled .class files would also change.

The importance of task inputs and outputs

You know what task inputs and outputs are now, but so what? Well it turns out that they play a crucial role in at least 3 key areas of Gradle functionality.

1. Up-to-date checks

Gradle’s incremental build feature helps your build avoid doing the same work more than once. For example, does Gradle really need to recompile your code if nothing’s changed?

Thankfully not. The way Gradle knows if a task should be executed again is using inputs and outputs. Every time a task is executed it takes a fingerprint of the inputs and outputs, and stores the fingerprints ready for next time the task is executed. If an input or output hasn’t changed, then the calculated fingerprint will be the same.

If all inputs and all outputs of a task have the same fingerprint as the last execution, the task can be skipped. Or in Gradle terminology the task is marked as UP-TO-DATE.

How up-to-date checks work with task inputs and outputs

How up-to-date checks work with task inputs and outputs

An example

Let’s take any Java Gradle project as an example. If we run the compileJava task on a clean project (one without a build directory), we get this output.

$ ./gradlew compileJava
> Task :compileJava

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

If we run the same task again, the output is this.

$ ./gradlew compileJava
> Task :compileJava UP-TO-DATE

BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date

You can clearly see that the compileJava task is marked as UP-TO-DATE, meaning Gradle skips running it completely. It knows that neither the inputs nor the outputs have changed.

For this small project, it makes a tiny 1s difference in performance. But for a large project, this incremental build feature can be a game-changer, saving developers a lot of time.

2. Linking task inputs and outputs

Another important use is to link the output of one task to the input of another. You can think about this using the producer/consumer analogy. A producer task creates some output that’s used as an input to a consumer task.

Linking tasks through inputs and outputs

Linking tasks through inputs and outputs

Since outputs can only be files or directories, the input of the consumer task is the same file, directory, or file collection created by the the producer task.

Linking task inputs and outputs like this has some important benefits:

  • Gradle automatically adds a task dependency from the consumer to producer, so when you run the consumer task the producer task runs first.
  • When the outputs of the producer task change, the consumer task gets executed again since it’s no longer up-to-date.

An example

Imagine we have a Gradle project which does some very simple string manipulation on movie quotes. 🎬

There are two tasks

a) addQuotationMarks
  • input is a file containing a movie quote e.g. quote.txt containing Bond. James Bond.
  • output is a file containing the quote in quotation marks e.g. quote-with-quotation-marks.txt containing “Bond. James Bond.”
b) addQuoteSource
  • input a file containing a movie quote in quotation marks i.e. the output of addQuotationMarks
  • output is a file containing the movie quote in quotation marks along with the source movie the quote came from e.g. quote-with-source.txt containing “Bond. James Bond.” Dr. No (1963)

When we run the addQuoteSource task you can see that both tasks are executed, since the inputs of addQuoteSource are linked to the outputs of addQuotationMarks

$ ./gradlew addQuoteSource
> Task :addQuotationMarks
> Task :addQuoteSource

BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed

Linking the output of addQuoationMarks to the input of addQuoteSource is as simple as this.

tasks.register<AddQuoteSource>("addQuoteSource") {
    // any other task configuration
    inputFile = addQuotationMarks.get().outputFile
}
tasks.register('addQuoteSource', AddQuoteSource) {
    // any other task configuration
    inputFile = addQuotationMarks.get().outputFile
}

You can see the full example build script in this GitHub repository.

3. Using dependency configurations

In Gradle a dependency configuration (or just configuration) is a way of grouping dependencies to define their scope. For example, the java plugin adds the implementation configuration which is used to generate the compile and runtime classpaths.

One way configurations can be used is as a producer e.g. I want to add an artifact built by this project to the implementation configuration of any consuming projects.

Task outputs can be used to easily tell Gradle that an artifact produced by a task should be added to a specific configuration. This configuration can then be used to share the artifact between projects.

An example

Let’s reuse the movie quote project idea. 🎥

This time we have two subprojects:

  1. produce-quote containing the addQuotationMarks task
  2. consume-quote containing the addQuoteSource task

In produce-quote we create a custom configuration called quote. We then add the text file output by the addQuotationMarks task to this configuration using the artifacts syntax below.

configurations {
    create("quote")
}

artifacts {
    add("quote", addQuotationMarks)
}
configurations {
    quote
}

artifacts {
    quote(addQuotationMarks)
}

This means when the consume-quote subproject needs to consume the output file quote-with-quotation-marks.txt, it can do so using the subproject path and configuration name.

val quote by configurations.creating

dependencies {
    quote(project(mapOf("path" to ":produce-quote", "configuration" to "quote")))
}
configurations {
    quote
}

dependencies {
    quote(project(path: ":produce-quote", configuration: 'quote'))
}

This is in fact Gradle’s recommended way for sharing inputs and outputs across subprojects.

Assuming the consuming task addQuoteSource has an input inputsFiles of type ConfigurableFileCollection, you can wire in the artifact dependency like this.

tasks.register<AddQuoteSource>("addQuoteSource") {
    inputFiles.from(quote)
    // any other task configuration
}
tasks.register('addQuoteSource', AddQuoteSource) {
    inputFiles.from(configurations.quote)
    // any other task configuration
}

To see the full example, including how to get the expected file from the ConfigurableFileCollection, check out the GitHub repository.

How to declare task inputs and outputs

Task inputs and outputs are highly configurable using two main approaches:

  1. Create inputs and outputs that always apply to a task inside the task class.
  2. Add inputs and outputs dynamically on a case-by-case basis.

Before moving onto full implementation details, let’s quickly explore the full options for what types can be declared as inputs and outputs.

Type Inputs Outputs
String
File
Iterable of files (Iterable<File> e.g. FileTree or FileCollection)
Map of files (Map<String, File>)
Directory
Iterable of directories (Iterable<File>)
Map of directories (Map<String, File>)
Java classpath

Note that:

  • Strings are only supported for task inputs, not outputs. These are normally used for configuration options e.g. sourceCompatibility of the compileJava task type.
  • Task outputs can only be files or directories. If you think about a task creating some kind of artifact, this makes sense.

Task lazy configuration

Gradle has the concept of lazy configuration, which allows task inputs and outputs to be referenced before they’re actually set. This is done via a Property class type.

One advantages of this mechanism is that you can link the output file of one task to the input file of another, all before the filename has even been decided. The Property class also knows about which task it’s linked to, so using inputs and outputs in this way enables Gradle to automatically add the required task dependency.

To understand how this works, here are some input types and their equivalent property-based type.

Simple type Property-based types
String Property<String>
File RegularFileProperty
(extends Property<File>)
Iterable<File> ConfigurableFileCollection
ConfigurableFileTree
(both extend Property<Iterable<File>>)

It’s preferable to use the property-based type, due to the flexibility mentioned above. You’ll see these types used frequently if you read Gradle task code.

Learn more about these different types in the documentation linked at the end of the article.

1. Task class inputs & outputs

A good practice is to create a task class for your custom task. The class encapsulates the task action logic, but should also declare any inputs and outputs the task expects. To do this, we use annotations.

  • For task inputs we can use @Input, @InputFile, @InputDirectory, @InputFiles, @Classpath, and @CompileClasspath.
  • For task outputs we have @OutputFile, @OutputDirectory, @OutputFiles, @OutputDirectories.

Note that in Kotlin the annotations are prefixed with get:, so @InputFile becomes @get:InputFile.

An example

Here’s a simple task class that takes as input two quote files. The output is another file containing the result of joining the values from the input files.

abstract class JoinQuote : DefaultTask() {
    @get:InputFile
    abstract val firstInputFile : RegularFileProperty
    @get:InputFile
    abstract val secondInputFile : RegularFileProperty
    @get:OutputFile
    abstract val outputFile : RegularFileProperty
    init {
        firstInputFile.convention(project.layout.projectDirectory.file("quote-part-1.txt"))
        secondInputFile.convention(project.layout.projectDirectory.file("quote-part-2.txt"))
        outputFile.convention(project.layout.buildDirectory.file("full-quote.txt"))
    }
    @TaskAction
    fun join() {
        outputFile.get().asFile.writeText(
            firstInputFile.get().asFile.readText() + secondInputFile.get().asFile.readText()
        )
    }
}
abstract class JoinQuote extends DefaultTask {
    @InputFile
    final abstract RegularFileProperty firstInputFile = project.objects.fileProperty().convention(project.layout.projectDirectory.file('quote-part-1.txt'))
    @InputFile
    final abstract RegularFileProperty secondInputFile = project.objects.fileProperty().convention(project.layout.projectDirectory.file('quote-part-2.txt'))
    @OutputFile
    final abstract RegularFileProperty outputFile = project.objects.fileProperty().convention(project.layout.buildDirectory.file('full-quote.txt'))
    @TaskAction
    void join() {
        outputFile.get().asFile.text = firstInputFile.get().asFile.text + secondInputFile.get().asFile.text
    }
}
  1. the @get:InputFile and @get:OutputFile annotations mean Gradle knows these properties are inputs and outputs respectively, and treats them accordingly.
  2. the properties are of type RegularFileProperty, which extends Property<File>. Since the properties are abstract, Gradle assigns the values automatically.
  3. we call convention on each property to set a default value
  4. call the get() function to retrieve the property value in the task action

Let’s define an instance of the class with tasks.register<JoinQuote>("joinQuote").

After running ./gradlew joinQuote multiple times, Gradle knows it’s up to date.

$ ./gradlew joinQuote
> Task :joinQuote UP-TO-DATE

BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date

Let’s change the quote file quote-part-1.txt, or in other words change the inputs.

Now we’ll see that the joinQuote task is no longer up-to-date and Gradle executes it again.

$ ./gradlew joinQuote
> Task :joinQuote

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

Finally, if we execute the clean task before joinQuote, or in other words change the outputs, Gradle also knows to execute the task again.

$ ./gradlew clean joinQuote
> Task :clean
> Task :joinQuote

BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed

To try it out for yourself see the example in this GitHub repository.

Working with other input and output types

So you’ve seen how to use the @InputFile and @OutputFile annotations, create property values, and set defaults, but what about the other input and output types?

Well, here’s a task which uses all the types, for your reference.

abstract class AllTypes : DefaultTask() {
    //inputs
    @get:Input
    abstract val inputString: Property<String>
    @get:InputFile
    abstract val inputFile: RegularFileProperty
    @get:InputDirectory
    abstract val inputDirectory: DirectoryProperty
    @get:InputFiles
    abstract val inputFileCollection: ListProperty<RegularFile>
    @get:Classpath
    abstract val inputClasspath: ListProperty<RegularFile>

    // outputs
    @get:OutputFile
    abstract val outputFile: RegularFileProperty
    @get:OutputDirectory
    abstract val outputDirectory: DirectoryProperty
    @get:OutputFiles
    abstract val outputFiles: ListProperty<RegularFile>
    @get:OutputDirectories
    abstract val outputDirectories: ListProperty<Directory>

    init {
        inputString.convention("default value")
        inputFile.convention(project.layout.projectDirectory.file("default-file.txt"))
        inputDirectory.convention(project.layout.projectDirectory.dir("default-dir"))
        inputFileCollection.convention(
            listOf(
                project.layout.projectDirectory.file("default-file-1.txt"),
                project.layout.projectDirectory.file("default-file-2.txt")
            )
        )
        inputClasspath.convention(listOf(project.layout.projectDirectory.file("MyClass.class")))

        outputFile.convention(project.layout.buildDirectory.file("default-file.txt"))
        outputDirectory.convention(project.layout.projectDirectory.dir("default-dir"))
        outputFiles.set(
            listOf(
                project.layout.buildDirectory.file("default-file-1.txt").get(),
                project.layout.buildDirectory.file("default-file-2.txt").get()
            )
        )
        outputDirectories.set(
            listOf(
                project.layout.projectDirectory.dir("default-dir-1"),
                project.layout.projectDirectory.dir("default-dir-2")
            )
        )
    }
}
abstract class AllTypes extends DefaultTask {
    //inputs
    @Input
    final abstract Property<String> inputString = project.objects.property(String).convention("default value")
    @InputFile
    final abstract RegularFileProperty inputFile = project.objects.fileProperty().convention(project.layout.projectDirectory.file('default-file.txt'))
    @InputDirectory
    final abstract DirectoryProperty inputDirectory = project.objects.directoryProperty().convention(project.layout.projectDirectory.dir('default-dir'))
    @InputFiles
    final abstract ConfigurableFileCollection inputFileCollection = project.objects.fileCollection().from(project.layout.projectDirectory.file('default-file-1.txt'), project.layout.projectDirectory.file('default-file-2.txt'))
    @Classpath
    final abstract ConfigurableFileCollection inputClasspath = project.objects.fileCollection().from(project.layout.projectDirectory.file('MyClass.class'))

    // outputs
    @OutputFile
    final abstract RegularFileProperty outputFile = project.objects.fileProperty().convention(project.layout.buildDirectory.file('default-file.txt'))
    @OutputDirectory
    final abstract DirectoryProperty outputDirectory = project.objects.directoryProperty().convention(project.layout.projectDirectory.dir('default-dir'))
    @OutputFiles
    final abstract ConfigurableFileCollection outputFiles = project.objects.fileCollection().from(project.layout.buildDirectory.file('default-file-1.txt'), project.layout.buildDirectory.file('default-file-2.txt'))
    @OutputDirectories
    final abstract ConfigurableFileCollection outputDirectories = project.objects.fileCollection().from(project.layout.projectDirectory.dir('default-dir-1'), project.layout.projectDirectory.dir('default-dir-2'))
}

As with all the examples in this article, you can find the code in this GitHub repository.

2. Ad-hoc task inputs & outputs

When we can, it’s preferable to declare task inputs and outputs in a task class using annotations. That might not always be possible or desirable, for example if we’re working with a 3rd party task class or we just want to define everything inline without creating a class. In this case, dynamically assign a task’s inputs and outputs within the build script itself.

To illustrate this, here’s a task definition for emphasiseQuote which takes as inputs a quote file and emphasis character, and outputs a file containing the quote with the emphasis character appended on the end.

tasks.register("emphasiseQuote") {
    inputs.file("quote.txt")
    inputs.property("emphasisCharacter", "!")
    outputs.file(layout.buildDirectory.file("emphasised-quote.txt"))
    doLast {
        outputs.files.singleFile.writeText(inputs.files.singleFile.readText() + inputs.properties.get("emphasisCharacter"))
    }
}
tasks.register('emphasiseQuote') {
    it.inputs.file('quote.txt')
    it.inputs.property('emphasisCharacter', '!')
    it.outputs.file(layout.buildDirectory.file('emphasised-quote.txt'))
    it.doLast {
        outputs.files.singleFile.text = inputs.files.singleFile.text + inputs.properties.get('emphasisCharacter')
    }
}

For example, if quote.txt contains You’re gonna need a bigger boat, once we’ve run ./gradlew emphasiseQuote the output file would contain You’re gonna need a bigger boat!.

Some notes on this implementation:

  • define inputs by calling the appropriate function on the task’s inputs (see TaskInputs)
  • define outputs by calling the appropriate function on the task’s outputs (see TaskOutputs)
  • retrieve the inputs or outputs at execution time with getFiles() or getProperties() e.g. outputs.files above
  • if retrieving inputs by name, declare the input with a name using property​(String name, Object value) e.g. inputs.properties.get("emphasisCharacter") above

Feel free to try the full example for yourself.

Some more examples

There are many possible use cases involving inputs and outputs, so review this list of example projects to see which one covers your scenario.

  1. custom-task: creates a task class declaring inputs and outputs with annotations
  2. custom-task-define-inputs-and-outputs-externally: similar to custom-task, but this time we don’t rely on defaults and define the values of inputs and outputs outside the task class
  3. pre-packaged-task: uses an existing Gradle task class (in this case Copy) and demonstrates the up-to-date checks working
  4. ad-hoc-task: doesn’t use a task class, but instead defines an ad-hoc task, dynamically creating inputs and outputs
  5. linking-tasks: demonstrates how to link inputs & outputs between tasks in the same project
  6. sharing-outputs-between-projects: a similar outcome to linking-tasks, but this time we share task outputs between subprojects using dependency configurations
  7. all-types-custom-task: defines a dummy task to illustrate how to use all the different input and output types

If you think there’s something missing, leave a comment below and I’ll try to fill in the gap!

Final thoughts

You now have an understanding of what inputs and outputs are, why they’re important, and how you can start using them in tasks in your own project. There’s plenty more to learn on this topic, so I recommend the following Gradle documentation.

  • Authoring tasks: contains the full list of annotations to use with inputs & outputs
  • Lazy Configuration: goes into a lot more detail on the Property class discussed in this article

Finally, if you want to learn how to create custom tasks and plugins to help scale your Gradle projects, sign up to the Gradle Hero course today and check out Chapter Five: Organising Gradle Projects Effectively.