skip to content

Task Actions & Graph API

What a task actually does at execution time — its ordered list of actions — and the APIs for inspecting or reacting to the resolved task graph. Interviewers ask because it forces you to separate configuration-time code from execution-time code.

on this pageshow

explore

questions

20

In a Gradle task definition, what is the difference between code in the configuration block and code inside doFirst{}/doLast{}, and when does each run?

level: juniorimportance: must knowfreq 80%

answer

  1. config phase vs execution phase
  2. register body = configuration
  3. doLast closure = execution-only
  4. actions skip when UP-TO-DATE
  5. work in actions, wiring in config

basics

~10 s

Configuration-block code runs during the configuration phase for every build invocation. doFirst{}/doLast{} are action closures that run only in the execution phase, and only if the task actually executes.

solid answer

~40 s

Gradle runs in phases: **initialization**, **configuration**, then **execution**. Code written directly in a `tasks.register("x") { ... }` block (the configuration block) runs during the *configuration* phase — every build, regardless of whether the task ends up in the task graph or runs. Code inside `doFirst { }` and `doLast { }` is added to the task's *action list* and runs during the *execution* phase, only if the task is actually selected and not up-to-date/skipped. So setting properties, wiring inputs/outputs, and dependencies belongs in the configuration block; the actual work (file copying, running a tool, printing results) belongs in `doLast`. Putting work in the configuration block makes it run on *every* invocation — a classic performance bug and the core idea behind configuration avoidance.

code

kotlin · 9 lines
kotlin
tasks.register("report") {
    group = "verification"            // configuration phase (every build)
    val outFile = layout.buildDirectory.file("report.txt")
    outputs.file(outFile)
    doLast {                          // execution phase (only if run)
        outFile.get().asFile.writeText("done")
        println("wrote report")
    }
}

go deeper

for a junior

Know the basic split: register-block code configures (always runs), doLast does the actual work at execution time.

for a middle

Articulate the three phases and that actions are skipped for up-to-date/skipped tasks; tie to where to place side effects.

for a senior

Connect to configuration avoidance and configuration-time performance; explain why side effects in config break incremental builds and config cache.

for a principal

Frame as a team convention: enforce 'no side effects at configuration time' to keep configuration cache compatible and builds fast at scale.

## The three build phases Every Gradle build runs in three ordered phases: 1. **Initialization** — Gradle decides which projects participate (settings.gradle). 2. **Configuration** — Gradle evaluates build scripts and *configures* every task object it needs to know about. The configuration block of a task runs here. 3. **Execution** — Gradle works out the requested task graph and runs the **actions** of each selected task, in order. ## Configuration block vs action block When you write: ```kotlin tasks.register("greet") { // <-- configuration block group = "demo" description = "says hi" doLast { // <-- action block (execution phase) println("hello") } } ``` The body of the `register` lambda (`group =`, `description =`, the *call* to `doLast`) executes during the **configuration phase**. The closure *passed to* `doLast` is stored in the task's action list and only invoked during the **execution phase**, and only if `greet` is actually run. ## Why it matters - Anything expensive or with side effects (reading files, calling external services, `println` of results) put **inside an action** (`doLast`), never in the configuration block. Configuration-block code runs on *every* build — even `./gradlew help` — which slows down every invocation. - `doFirst`/`doLast` only fire when the task executes. If the task is `UP-TO-DATE`, `SKIPPED`, or `NO-SOURCE`, its actions do **not** run. - This distinction is the foundation of **configuration avoidance**: `tasks.register` (lazy) configures a task only when it's needed, versus `tasks.create` (eager) which configures immediately. ## The action list A task is really an ordered list of `Action<Task>` objects. `doFirst` prepends, `doLast` appends. A task with no actions (a lifecycle task like `build`) simply has an empty action list and does nothing itself.

  • Why is putting println("building") directly in the register block usually a mistake?
    It runs during the configuration phase on every single invocation (even unrelated tasks like `help`), not when the task executes. Side effects belong in a doLast action.
  • If a task is UP-TO-DATE, do its doLast actions run?
    No. When a task is up-to-date (or skipped/no-source), its action list is not executed at all.

The configuration block is writing the recipe card (always done when you open the cookbook); doFirst/doLast are actually cooking — only happens when you decide to make that dish.

saying these in an interview costs you the question

  • Saying doLast runs during the configuration phase.
  • Claiming the whole register{} body runs only when the task executes.
  • Putting expensive work or println of results in the configuration block.

context

open as a page

What does task.onlyIf {} do in Gradle, and what happens to a task whose onlyIf predicate returns false?

level: juniorimportance: must knowfreq 70%

basics

~10 s

onlyIf {} attaches a predicate evaluated right before the task runs. If it returns false, Gradle skips the task's actions and reports it as SKIPPED. If true (or absent), the task executes normally.

open as a page

What is the difference between the tasks a user requested on the command line and the tasks Gradle actually schedules and executes?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Requested tasks are the names you type on the command line (e.g. gradle build). Executed tasks are those plus all their dependencies, which Gradle computes and runs in dependency order.

open as a page

What is gradle.taskGraph.whenReady{} and when does the closure it registers actually run?

level: juniorimportance: must knowfreq 55%

basics

~10 s

whenReady registers a callback that runs once Gradle has finished building the task execution graph for the build, after configuration but before any task executes.

open as a page

How do doFirst{} and doLast{} compose into a task's action list, and what is the execution order if you call them multiple times?

level: middleimportance: must knowfreq 65%

basics

~20 s

A task holds an ordered list of actions. doLast appends to the end; doFirst prepends to the front. Multiple doFirst calls stack so the last-added doFirst runs first; doLast actions run in the order added.

open as a page

How does onlyIf differing from an up-to-date check (outputs.upToDateWhen)? When would you reach for each?

level: middleimportance: must knowfreq 60%

basics

~20 s

onlyIf decides whether the task should run at all (false → SKIPPED). The up-to-date check decides whether work is unnecessary because inputs/outputs are unchanged (→ UP-TO-DATE). onlyIf runs first; if it passes, Gradle then does the up-to-date check.

open as a page

How would you make build logic run only when a specific task (say `test`) is actually part of the current build, and why not just check the command-line arguments?

level: middleimportance: must knowfreq 45%

basics

~10 s

Use gradle.taskGraph.whenReady { if (hasTask(":test")) { ... } }. Checking command-line args fails because tasks like test are often pulled in transitively (by build, check), not typed directly.

open as a page

Inside whenReady, how do you inspect what the build is about to do using allTasks and hasTask, and what do they return?

level: middleimportance: must knowfreq 48%

basics

~10 s

graph.allTasks returns the ordered list of every Task that will execute; graph.hasTask checks whether a specific task (by path or instance) is in the graph. Both reflect the fully built graph.

open as a page

What does `--dry-run` do, and how is it useful for understanding the difference between requested and executed tasks?

level: juniorimportance: should knowfreq 40%

basics

~10 s

--dry-run (or -m) makes Gradle resolve and print every task it would execute, each marked SKIPPED, without running any task actions. It shows the full executed set for a given request.

open as a page

What is a lifecycle (aggregate) task with no actions, and how does it differ from a task that has doLast actions?

level: middleimportance: should knowfreq 45%

basics

~20 s

A lifecycle task has an empty action list — it does no work itself and exists only to group/depend on other tasks (like build or check). A task with doLast actions actually performs work when executed.

open as a page

What is the difference between gating a task with onlyIf {} versus a configuration-time if-block that conditionally registers or wires the task?

level: middleimportance: should knowfreq 45%

basics

~20 s

A configuration-time if-block decides whether the task is even created/wired while scripts are evaluated. onlyIf decides at execution time whether an existing task runs. Configuration-time conditions can use only configuration-known values; onlyIf can use values known later.

open as a page

Inside a build, how can you programmatically inspect the full set of tasks Gradle has scheduled, and what should you be careful about when doing so?

level: middleimportance: should knowfreq 30%

basics

~10 s

Use gradle.taskGraph.allTasks after the graph is ready (e.g. in whenReady) to get the ordered List<Task> of scheduled tasks. Match on task.path (absolute) for precision in multi-project builds.

open as a page

What is addTaskExecutionGraphListener and how does it relate to whenReady?

level: middleimportance: should knowfreq 30%

basics

~10 s

addTaskExecutionGraphListener registers a TaskExecutionGraphListener whose graphPopulated method fires when the graph is ready — the same moment as whenReady. whenReady is just closure-based sugar for the same hook.

open as a page

Why is doing work in doFirst/doLast (instead of the task configuration block) important for the configuration cache and incremental builds?

level: seniorimportance: should knowfreq 50%

basics

~20 s

The configuration cache stores the configured task graph and reuses it, skipping the configuration phase. Side effects must live in actions (doFirst/doLast/@TaskAction) so they still run on cache hits; configuration-time side effects would be lost or break the cache.

open as a page

What are common pitfalls when writing onlyIf predicates, particularly around side effects, exceptions, and lazy configuration?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Keep onlyIf predicates pure and cheap: no mutating state, no heavy I/O, and don't throw (an exception there fails the build, not a clean skip). Read inputs lazily via Providers, and don't rely on configuration order since the body runs at execution.

open as a page

When you call onlyIf multiple times on a task, how are the predicates combined, and how can you make a skip reason visible in the logs?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Multiple onlyIf predicates are ANDed: the task runs only if all return true. To surface why it skipped, use the onlyIf(reason) { } overload (Gradle 7.6+); the reason appears in --info/--debug output when the task is skipped.

open as a page

What is `gradle.startParameter.taskNames`, when is it a legitimate thing to read, and what are its limits?

level: seniorimportance: should knowfreq 25%

basics

~20 s

startParameter.taskNames is the list of raw task-name strings (and selectors) the user requested on the command line. It reflects intent, not the resolved graph, so it's fine for telemetry/diagnostics but wrong for 'will task X run?' decisions.

open as a page

How would you use gradle.taskGraph.whenReady to fail a build fast based on the set of tasks about to run?

level: seniorimportance: should knowfreq 32%

basics

~10 s

Register whenReady, inspect graph.allTasks/hasTask, and throw a GradleException if an invalid or risky combination is detected — so the build aborts before any task action runs.

open as a page

How does whenReady differ from afterEvaluate, and why might you choose one over the other?

level: seniorimportance: should knowfreq 38%

basics

~10 s

afterEvaluate runs at the end of a project's configuration (per project, graph not yet built). whenReady runs once after the whole task graph is built. Use whenReady when you must know the requested/executed tasks.

open as a page

What was the `<<` (leftShift) operator on tasks, why was it removed, and what replaces it?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

In old Groovy DSL, task foo << { ... } was shorthand for adding a doLast action. It was deprecated in Gradle 3.x and removed in Gradle 5.0 because it blurred configuration vs action code. Use doLast { } instead.

open as a page