skip to content

Authoring Task Types

Writing your own task class: the action method, annotated inputs and outputs, normalization, incremental execution, cacheability, and dependency wiring. Interviewers ask because a well-authored task type is what makes a build both fast and correct.

on this pageshow

questions

page 1 of 2

What does the @CacheableTask annotation do, and what is the difference between marking a task cacheable and the build cache simply being enabled?

level: juniorimportance: must knowfreq 62%

answer

  1. type-level opt-in
  2. key = hash of inputs
  3. superset of up-to-date
  4. enable != mark cacheable
  5. cacheIf / doNotCacheIf

basics

~10 s

@CacheableTask on a task type opts that type into the build cache, so its outputs can be stored and reused. Enabling the build cache alone does nothing unless task types are marked cacheable.

solid answer

~40 s

The build cache is a key-value store of task outputs keyed by a hash of the task's inputs. Enabling it (`--build-cache` or `org.gradle.caching=true`) turns the mechanism on, but a task only participates if its **type** is annotated `@CacheableTask`. By default custom and most built-in tasks are NOT cacheable — they're only up-to-date checked locally. Marking a task `@CacheableTask` tells Gradle: this task's inputs are fully and correctly declared and its outputs are reproducible, so it's safe to store outputs under a cache key and reuse them — even across machines via a remote cache. You can also flip individual instances on/off with `task.outputs.cacheIf { ... }` / `doNotCacheIf { }`.

code

kotlin · 17 lines
kotlin
@CacheableTask
abstract class GenerateReport : DefaultTask() {
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sources: ConfigurableFileCollection

    @get:OutputFile
    abstract val report: RegularFileProperty

    @TaskAction
    fun run() { /* produce report from sources */ }
}

// per-instance refinement
tasks.named<GenerateReport>("generateReport") {
    outputs.cacheIf { sources.files.size < 10_000 }
}

go deeper

for a junior

Know @CacheableTask is a class-level opt-in and that enabling the cache is a separate switch.

for a middle

Explain key = hash of inputs, cache as a superset of up-to-date, and per-instance cacheIf/doNotCacheIf.

for a senior

Discuss why opt-in exists (correctness/determinism guarantees) and which built-ins are/aren't cacheable and why.

for a principal

Frame local vs remote cache strategy, CI hit-rate, and the org-wide correctness bar before broadly enabling caching.

## What the build cache is Gradle's **build cache** is a key-value store. The *key* is a hash computed from everything that can affect a task's outputs (its inputs); the *value* is a packaged copy of that task's output files. When Gradle is about to run a task, it computes the cache key; on a hit it unpacks the stored outputs instead of executing the task. This is a superset of the **up-to-date** (incremental) check: up-to-date only avoids re-running when *this build directory* already holds the right outputs, whereas the cache can supply outputs produced by an *earlier build or a different machine* (with a **remote** cache). ## Enabling vs. marking cacheable — two separate switches 1. **Enabling the cache** turns the machinery on: `--build-cache` on the CLI, or `org.gradle.caching=true` in `gradle.properties`. This alone caches *nothing extra* unless tasks opt in. 2. **Marking a task type cacheable** is `@CacheableTask` on the task *class*. Without it, even with the cache enabled, the task is never stored or loaded from the cache — it only benefits from the in-place up-to-date check. ```kotlin @CacheableTask abstract class GenerateReport : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sources: ConfigurableFileCollection @get:OutputFile abstract val report: RegularFileProperty @TaskAction fun run() { /* ... */ } } ``` ## Why opt-in? Caching is only safe when a task's inputs are **completely and correctly declared** and its outputs are **deterministic** (no embedded timestamps/absolute paths, same inputs ⇒ same bytes). Gradle can't prove this, so it makes you assert it by annotating the type. A task with an undeclared input (e.g. it reads an env var or a file it never registered as `@InputFile`) would otherwise produce stale cache hits. ## Per-instance control `@CacheableTask` is a *type-level* default. You can refine per instance: - `outputs.cacheIf { spec }` — only cache when the predicate holds. - `outputs.doNotCacheIf("reason") { spec }` — exclude when a predicate holds. All `cacheIf` must be true and no `doNotCacheIf` true for the instance to be cached. ## Built-ins Many core tasks (`JavaCompile`, `Test`, `Checkstyle`, …) are already `@CacheableTask`. Tasks like `Copy`/`Jar` are intentionally *not* cacheable because packing/unpacking from the cache is rarely cheaper than just doing the copy.

  • If I add org.gradle.caching=true but my custom task isn't @CacheableTask, what happens?
    Nothing for that task — it still runs (or is up-to-date) but is never stored to or loaded from the cache. Only its inputs/outputs are tracked for the in-place up-to-date check.
  • Where does the cache live by default and how does a remote cache differ?
    A local directory cache (under the Gradle user home) is on by default once caching is enabled. A remote cache (e.g. an HTTP/Develocity cache) is shared across machines/CI, letting one machine reuse another's outputs.

Enabling the cache installs the shared fridge; @CacheableTask is each cook labelling a dish 'safe to share' — an unlabelled dish stays in their own kitchen even though the fridge is on.

saying these in an interview costs you the question

  • Thinking org.gradle.caching=true alone makes all tasks cacheable.
  • Claiming @CacheableTask is the same as @UpToDate or replaces incremental checks.
  • Marking a task cacheable without verifying its outputs are deterministic.

context

open as a page

What is a CommandLineArgumentProvider in Gradle, and why would you use one instead of just adding strings to a task's args?

level: juniorimportance: must knowfreq 45%

basics

~10 s

It's a small object that supplies command-line arguments lazily via asArguments(). You register it on a task (e.g. JavaExec/Test) so the actual arguments are computed at execution time rather than hardcoded as plain strings.

open as a page

What is DefaultTask and how do you create a custom task type by subclassing it?

level: juniorimportance: must knowfreq 70%

basics

~10 s

DefaultTask is Gradle's base class for tasks. You create a custom task type by subclassing it and adding one method annotated with @TaskAction, which holds the work the task runs when executed.

open as a page

What does `dependsOn` do in Gradle, and how is a task dependency different from controlling the mere ordering of two tasks?

level: juniorimportance: must knowfreq 80%

basics

~20 s

dependsOn declares that one task requires another to run first; if you ask for task A, Gradle also runs its dependencies. A pure ordering hook only sequences tasks that are already scheduled — it never adds a task to the build.

open as a page

What is the purpose of annotating task properties with @Input, @InputFile, and @OutputFile in a custom Gradle task?

level: juniorimportance: must knowfreq 70%

basics

~10 s

They tell Gradle which properties are a task's inputs and outputs so Gradle can decide if the task is up-to-date and skip running it when nothing relevant changed.

open as a page

Why must a @CacheableTask declare path sensitivity (e.g. @PathSensitive(RELATIVE)) on its file inputs, and what goes wrong if you leave the default ABSOLUTE sensitivity?

level: middleimportance: must knowfreq 55%

basics

~10 s

Absolute paths differ per machine/checkout, so absolute path sensitivity makes the cache key machine-specific and kills cross-machine hits. Cacheable tasks should use RELATIVE (or NAME_ONLY/NONE) so the key reflects content, not location.

open as a page

How does Gradle track the inputs of a CommandLineArgumentProvider, and what role does @Nested play?

level: middleimportance: must knowfreq 40%

basics

~10 s

The task's provider collection is annotated @Nested, so Gradle recursively scans each provider's own @Input/@InputFile properties and folds them into the task's input fingerprint. That's how dynamic args participate in up-to-date checks.

open as a page

In a custom DefaultTask, what is the difference between code placed in the constructor and code placed in the @TaskAction method?

level: middleimportance: must knowfreq 55%

basics

~10 s

Constructor code runs at configuration time (every build, whenever the task is realized). @TaskAction code runs at execution time, only when the task actually runs. Put the real work in @TaskAction, not the constructor.

open as a page

How do you register a custom DefaultTask subclass, and why prefer tasks.register over tasks.create?

level: middleimportance: must knowfreq 60%

basics

~10 s

Register a task type with tasks.register("name", MyTask::class) { ... }. Prefer register over create because register is lazy: the task is only configured if it is actually needed, which speeds up configuration.

open as a page

What is an inferred (implicit) task dependency, and how does wiring a producer's output Provider into a consumer's input property make explicit `dependsOn` unnecessary?

level: middleimportance: must knowfreq 70%

basics

~20 s

If you connect a producing task's output Provider (e.g. its RegularFileProperty) directly to a consuming task's input property, Gradle sees the output 'carries' its producing task and automatically adds the dependency — no manual dependsOn needed.

open as a page

Walk through how you handle each ChangeType (ADDED, MODIFIED, REMOVED) in an incremental task action, and what goes wrong if you ignore REMOVED.

level: middleimportance: must knowfreq 45%

basics

~20 s

ADDED and MODIFIED mean regenerate the output for that file; REMOVED means delete the output that file produced. If you ignore REMOVED, deleted sources leave stale generated outputs behind, so the output directory no longer matches the inputs.

open as a page

What is an incremental task in Gradle, and why would you author one instead of relying on Gradle's normal UP-TO-DATE checking?

level: middleimportance: must knowfreq 55%

basics

~20 s

A task that, when re-run, processes only the input files that changed since the last run instead of all of them. You author one to avoid redoing work for the whole input set when a few files changed.

open as a page

What is the difference between @Input and @InputFile (and @InputFiles / @InputDirectory), and when would you use each?

level: middleimportance: must knowfreq 60%

basics

~10 s

@Input snapshots a scalar value (string, number, enum). @InputFile/@InputFiles/@InputDirectory track file or directory content by hashing the bytes. Use @Input for plain values, the file annotations when a File/FileCollection's content matters.

open as a page

What does @PathSensitive do on a task input, and why does it affect up-to-date checks and the build cache?

level: middleimportance: must knowfreq 55%

basics

~10 s

@PathSensitive tells Gradle which part of a file's path counts when fingerprinting an input. With RELATIVE, only the relative path plus content matters, so moving the project directory still hits the cache.

open as a page

How do @Classpath and @CompileClasspath differ from @PathSensitive(RELATIVE) for declaring classpath-style inputs, and when do you use each?

level: seniorimportance: must knowfreq 45%

basics

~10 s

@Classpath fingerprints a list of jars by content but ignores order-insensitive path noise and applies jar-aware normalization. @CompileClasspath goes further, hashing only the public ABI of classes so non-API changes don't bust the cache.

open as a page

How does an incremental task using InputChanges differ from a normal task that just declares @InputDirectory/@OutputDirectory, and when is the extra complexity justified?

level: juniorimportance: should knowfreq 38%

basics

~20 s

A normal task with declared inputs/outputs is skipped entirely if nothing changed, but re-runs fully if anything changed. An incremental task re-runs but processes only the changed files. The complexity is justified when per-file work is expensive and only a few of many files change.

open as a page

Why should a CommandLineArgumentProvider read its values through Provider/Property and resolve them inside asArguments() rather than capturing plain values at configuration time?

level: middleimportance: should knowfreq 25%

basics

~10 s

Using Property/Provider keeps the values lazy: they're resolved when asArguments() runs at execution time, so later configuration changes and convention/defaults are picked up. Capturing plain values early freezes stale or unconfigured data.

open as a page

What is `finalizedBy`, what are its key semantics (especially on failure), and what is a canonical use case?

level: middleimportance: should knowfreq 50%

basics

~20 s

a.finalizedBy(b) schedules b to run after a, even if a fails. It's used for guaranteed cleanup or teardown — like stopping a server or collecting reports — that must happen whether or not the main task succeeded.

open as a page

Compare `mustRunAfter` and `shouldRunAfter`. When would you choose one over the other, and how do they interact with parallel execution?

level: middleimportance: should knowfreq 55%

basics

~20 s

Both only order tasks that are already scheduled, never adding tasks. mustRunAfter is a hard ordering constraint Gradle always honors. shouldRunAfter is a soft preference Gradle can break to avoid a cycle or to enable more parallelism.

open as a page

What does the @LocalState annotation declare on a task property, and why would you use it instead of @OutputDirectory?

level: middleimportance: should knowfreq 30%

basics

~10 s

@LocalState marks a directory or file holding a task's intermediate, machine-specific state. Gradle tracks it for up-to-date checks but does NOT store it in the build cache, because it shouldn't be shared across machines.

open as a page

When and how would you use @OutputFiles or @OutputDirectories (the map-accepting forms) instead of single @OutputFile/@OutputDirectory properties?

level: middleimportance: should knowfreq 28%

basics

~10 s

Use @OutputFiles/@OutputDirectories when a task produces multiple, dynamically-keyed outputs. Backing the property with a Map<String, File> gives each output a stable identity key, which Gradle uses for reliable up-to-date checks and overlapping-output handling.

open as a page

What do @Internal and @Optional do on task properties, and when do you reach for them?

level: middleimportance: should knowfreq 45%

basics

~10 s

@Internal marks a property as deliberately NOT part of inputs/outputs, so it's excluded from up-to-date checks. @Optional says an input/output property is allowed to be unset/null without failing validation.

open as a page

What do @IgnoreEmptyDirectories and @NormalizeLineEndings do on a task input, and what kinds of false cache misses do they prevent?

level: middleimportance: should knowfreq 35%

basics

~10 s

@IgnoreEmptyDirectories drops empty directories from the input fingerprint so they don't trigger reruns. @NormalizeLineEndings hashes text files ignoring CRLF vs LF differences, so a line-ending-only change doesn't bust the cache.

open as a page

What is @DisableCachingByDefault, when would you apply it to a task type, and how does it interact with @CacheableTask and cacheIf?

level: seniorimportance: should knowfreq 33%

basics

~10 s

@DisableCachingByDefault marks a task type as intentionally not cacheable and records a reason, so it isn't a candidate for caching and validation won't nag about it. It's the explicit opposite of @CacheableTask.

open as a page

What ingredients make up a cacheable task's build cache key, and how can two builds that you think are identical end up with different keys?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The key hashes the task's implementation (type + classpath), each @Input value, and the fingerprint of all file inputs. Any difference — a changed annotation processor, a different input file content/path, or a plugin upgrade — changes the key.

open as a page

How do you keep CommandLineArgumentProvider arguments that reference file paths from breaking a relocatable build cache?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Track the file via @InputFile/@Classpath with an appropriate @PathSensitivity (often NONE or RELATIVE) so the cache key depends on content, not the absolute path. The absolute path appears only in the returned argument string, which isn't fingerprinted.

open as a page

Why are modern custom task types declared abstract, and how does Gradle provide the missing implementations and services?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Declaring a task abstract lets Gradle generate the concrete subclass at runtime. Gradle supplies implementations for abstract managed-property getters (like Property) and injects services through @Inject constructor params or abstract getters.

open as a page

How do you wire dynamic, computed task dependencies inside a custom task type — for example via `TaskDependency` / `Buildable` or a `@TaskAction`-time set, and what is `getTaskDependencies()` for?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Task dependencies are computed at graph-construction time, not execution time. To make them dynamic you pass a Callable/closure or Provider to dependsOn, or implement Buildable.getBuildDependencies() on an input value so the value 'carries' the tasks that build it.

open as a page

A custom cacheable task produces some real outputs, keeps an incremental scratch directory, and a separate task wipes that scratch directory on demand. Walk through which annotations you'd use where, and why.

level: seniorimportance: should knowfreq 18%

basics

~10 s

Real artifacts → @OutputFiles/@OutputDirectories. The incremental scratch directory → @LocalState (tracked, not cached). The wipe task's deleted path → @Destroys so it never races the producer under --parallel. Inputs stay @Input/@InputFiles with path sensitivity.

open as a page

What is the @Destroys annotation for, and how does it affect task scheduling and parallel execution?

level: seniorimportance: should knowfreq 22%

basics

~20 s

@Destroys marks a path a task removes (e.g. a clean task deleting build/). Gradle uses it to schedule destroyer tasks so they never overlap with tasks that produce or consume those same paths, preventing data races in parallel builds.

open as a page

showing 1–30 of 40