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 it is your task needs to accomplish.

We run tasks on the command line. For example, running ./gradlew compileJava will take your project’s .java files and compile them into .class files.

Task inputs and outputs

For you 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

You’ll find that most official Gradle tasks have inputs and outputs. Can you guess what the compileJava task inputs and outputs are?

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

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

So 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?

Well 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

An example

Let’s take any Java Gradle project as an example. If we run the compileJava task on a clean project (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. 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. One way to think about this is 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

Outputs can only be files or directories. This effectively makes the input of the consumer task the same file, directory, or file collection as the producer task.

This has some important benefits:

  • Gradle automatically adds a task dependency from the consumer to the producer. This means when you run the consumer task it first runs the producer task.
  • when the outputs of the producer task change, the consumer task will get 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
}

You can see the full example build.gradle is this GitHub repository.

3. Using dependency configurations

In Gradle a dependency configuration (or just configuration) is a way of grouping together 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’ll have two subprojects:

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

In produce-quote we can create a custom configuration called quote. We can then add the output text file of the addQuotationMarks task to this configuration very simply.

configurations {
    quote
}

artifacts {
    quote(addQuotationMarks)
}

This means that if the consume-quote subproject needs to consume the output file quote-with-quotation-marks.txt it could do that like this.

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(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. You can create inputs and outputs that always apply to a task inside the task class, or add them 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

Type support for Gradle task inputs & outputs

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 are 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 normally 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.

An example

Here’s a simple example of a 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 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 @InputFile annotation means that Gradle knows these properties are inputs, and it treats them accordingly.
  2. the inputs are of type RegularFileProperty, which extends Property<File>. We create such a property using the ObjectFactory class, which contains helper methods for creating different types of properties.
  3. on the returned property from project.objects.fileProperty() we call convention which just allows us to set a default value
  4. finally, when we need the actual property value in the task action, we call get() on the property

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 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, we can dynamically assign a task’s inputs and outputs within the build.gradle 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') {
    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()
  • if retrieving inputs by name, declare the input with a name using property​(String name, Object value)

Feel free to try the full example for yourself.

Some more examples

There are many possible use cases involving inputs and outputs, so check out this list of Gradle example projects to see if 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, please leave a comment below and I’ll try to fill in the gap!

Final thoughts

You should 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