What is a TaskExecutionListener in Gradle, and how do you register one to observe when tasks start and finish?
answer
- beforeExecute / afterExecute
- TaskState: failure, didWork, skipMessage
- gradle.addListener(...)
- taskGraph.beforeTask / afterTask
- register early (init/settings/plugin apply)
basics
~10 sA TaskExecutionListener gets a callback before each task runs (beforeExecute) and after it finishes (afterExecute). You register it via gradle.addListener(listener) in the build script or a plugin.
solid answer
~40 s`TaskExecutionListener` is a Gradle lifecycle listener interface with two callbacks: `beforeExecute(task)` fires just before a task action runs, and `afterExecute(task, state)` fires after it completes. The `state` is a `TaskState` exposing `getFailure()`, `getDidWork()`, `getSkipMessage()`, etc. You register it on the `Gradle` object with `gradle.addListener(myListener)` — typically inside a settings plugin, init script, or project plugin's `apply`. It observes execution of *every* task in the build, so it's used for cross-cutting concerns like custom timing, audit logs, or per-task profiling. Note: as of Gradle 7+ this API still works but is discouraged for build scans/profiling in favor of the Tooling-API `BuildEventListenerRegistry` + `OperationCompletionListener`, which is configuration-cache compatible.
code
kotlin · 4 linesgradle.taskGraph.beforeTask { task -> logger.lifecycle("-> ${task.path}") }
gradle.taskGraph.afterTask { task ->
if (task.state.failure != null) logger.error("${task.path} FAILED")
}go deeper
Name the two callbacks (beforeExecute/afterExecute) and that you register with gradle.addListener or taskGraph.beforeTask/afterTask.
Explain TaskState fields and that the listener observes every task, plus where to register it (init/settings/plugin apply, not in a task action).
Discuss the configuration-cache incompatibility and when to prefer OperationCompletionListener instead.
Frame listener strategy across a multi-module org: prefer cache-safe Tooling-API listeners in a convention plugin, avoid global mutable state, standardize timing/audit hooks centrally.
## What problem listeners solve Gradle runs a build in three phases: **initialization** (settings, which projects), **configuration** (every build script is evaluated, the task graph is built), and **execution** (selected tasks run). Sometimes you want to *observe* what happens during execution without modifying individual tasks — for example, to log timings, collect metrics, or audit which tasks did real work. Listeners are the cross-cutting hook for that. ## TaskExecutionListener `org.gradle.api.execution.TaskExecutionListener` has two methods: - `beforeExecute(Task task)` — called immediately before the task's actions run. - `afterExecute(Task task, TaskState state)` — called after the task finishes (whether it succeeded, failed, was skipped, or was up-to-date). `TaskState` is the key data object: `state.failure` (a Throwable or null), `state.didWork` (did it actually do something vs. up-to-date), `state.skipped` / `state.skipMessage`, `state.upToDate`, `state.noSource`. This lets a single listener observe outcomes for every task in the build. ## How to register You attach a listener to the `Gradle` object (accessible as `gradle` in a build script, `settings.gradle`, or via `project.gradle`). The general method is `gradle.addListener(Object)` — Gradle inspects the object and routes it to the right listener type(s). ```kotlin gradle.addListener(object : TaskExecutionListener { override fun beforeExecute(task: Task) { task.extensions.extraProperties["start"] = System.nanoTime() } override fun afterExecute(task: Task, state: TaskState) { val start = task.extensions.extraProperties["start"] as Long val ms = (System.nanoTime() - start) / 1_000_000 println("${task.path} took ${ms}ms (failure=${state.failure})") } }) ``` There are also convenience closure forms: `gradle.taskGraph.beforeTask { ... }` and `gradle.taskGraph.afterTask { ... }` register the same hooks without writing a full interface. ## Where to register Register early — usually in an init script, a settings plugin, or a project plugin's `apply` — so the listener is in place before execution begins. Registering inside a task action is too late. ## Caveat: configuration cache These mutable, global listeners (`TaskExecutionListener`, `gradle.addListener`) are **not compatible with the configuration cache** because they hold live state across the build. Modern Gradle steers profiling/observation toward the Tooling-API `OperationCompletionListener` registered through `BuildEventsListenerRegistry`, which *is* cache-safe.
- What does TaskState.didWork tell you that the absence of a failure does not?didWork is false when the task was UP-TO-DATE, skipped, or had no source — i.e. it succeeded but did no real work. failure==null only means it didn't throw; didWork distinguishes a real run from a no-op.
- Why might gradle.addListener cause a configuration-cache problem?It registers a mutable global object that carries live state across the build; the configuration cache serializes the configured state and cannot capture that listener safely, so it reports it as an incompatible API.
saying these in an interview costs you the question
- Claiming the listener fires during the configuration phase — it fires during execution.
- Saying afterExecute only runs on success; it runs on failure, up-to-date, and skipped tasks too.