skip to content

Build Completion Hooks

Reacting to the end of a build with buildFinished, and the flow API that replaces it in a configuration-cache-safe way. Asked because buildFinished is exactly the kind of hook modern Gradle is retiring.

on this pageshow

questions

5

How do you run code after a Gradle build finishes, and what does gradle.buildFinished give you access to?

level: juniorimportance: must knowfreq 55%

answer

  1. fires after build, success or failure
  2. BuildResult.getFailure() null on success
  3. classic teardown/cleanup hook
  4. NOT configuration-cache safe
  5. register in init script or plugin

basics

~20 s

Register a callback with gradle.buildFinished { result -> ... }. It runs once the build ends and gives you a BuildResult with result.failure, so you can do cleanup or reporting whether the build passed or failed.

solid answer

~40 s

`gradle.buildFinished` is a lifecycle hook that fires once after the build completes — on success and on failure. The closure receives a `BuildResult` exposing `getFailure()` (the `Throwable` that ended the build, or null on success) and `getAction()` (e.g. "Build"). You typically register it in an init script or plugin to run cleanup, stop daemons you started, or emit a custom report. It runs even if a task failed, which makes it the classic place for teardown. ```kotlin gradle.buildFinished { if (it.failure != null) println("Build failed: ${it.failure?.message}") } ``` Note: `buildFinished` is **not** compatible with the configuration cache. Modern Gradle steers you to the Flow API (`FlowScope`/`FlowAction`) instead for new code.

code

kotlin · 7 lines
kotlin
gradle.buildFinished { result ->
    if (result.failure != null) {
        logger.error("Build failed", result.failure)
    } else {
        logger.lifecycle("Build succeeded")
    }
}

go deeper

for a junior

Know the closure form gradle.buildFinished { ... }, that it runs at the end, and that result.failure tells you if it failed.

for a middle

Explain BuildResult contents, where you'd register it (init script vs plugin), and that it fires on the failure path for teardown.

for a senior

Flag the configuration-cache incompatibility and that the Flow API is the replacement for new code.

for a principal

Frame org-wide build telemetry/reporting via init scripts and the migration policy away from buildFinished across many repos.

## What `buildFinished` is Gradle runs in phases: **initialization**, **configuration**, then **execution**. The `Gradle` object (available as `gradle` in build scripts, settings scripts, and init scripts) exposes lifecycle callbacks. `gradle.buildFinished(closure)` registers an action that runs **once**, after the entire build has finished — regardless of whether it succeeded or failed. ## The `BuildResult` you receive The callback is handed a `org.gradle.BuildResult`: - `getFailure(): Throwable?` — the exception that terminated the build, or `null` if it succeeded. - `getAction(): String` — what Gradle was doing (typically `"Build"`). - `getGradle(): Gradle` — back-reference to the build. This is why `buildFinished` is the canonical teardown hook: it fires on the failure path too, so resources you spun up (test fixtures, external processes, temp directories, a started database) get cleaned up. ## Where to register it - **Init script** (`~/.gradle/init.gradle(.kts)` or `--init-script`) — applies to every build; good for org-wide reporting. - **Settings script** or a **plugin** — scoped to one project tree. ## The big caveat: configuration cache `buildFinished` captures a closure that may reference live build model objects (the `Project`, `Task` graph, services). Those cannot be serialized, so **`buildFinished` is incompatible with the configuration cache** and Gradle emits a problem when it is used under `--configuration-cache`. For new code Gradle recommends the **Flow API** (`FlowScope.always { ... }` registering a `FlowAction`), which declares its inputs as serializable `Property` values and therefore survives the cache. ```kotlin // Legacy completion hook gradle.buildFinished { result -> val outcome = if (result.failure == null) "OK" else "FAILED" logger.lifecycle("Build outcome: $outcome") } ``` ## Related hooks `buildFinished` is the last of a family: `gradle.beforeProject`/`afterProject`, `gradle.projectsEvaluated`, `gradle.taskGraph.whenReady`. `buildFinished` is strictly the post-execution one.

  • Does the callback run if a task in the build fails?
    Yes. That is the point of it — it runs on both the success and failure paths, with result.failure populated on failure, so it is safe for teardown.
  • Why might buildFinished print a deprecation/problem warning in a modern build?
    Because it is incompatible with the configuration cache; Gradle reports it as a problem and recommends migrating to the Flow API.

saying these in an interview costs you the question

  • Saying it only runs on success — it runs on failure too.
  • Claiming buildFinished is configuration-cache compatible.
  • Confusing it with afterProject (which is per-project, during configuration).

context

open as a page

Why is the Flow API (FlowScope/FlowAction) preferred over gradle.buildFinished, and how do you wire up a completion action with it?

level: seniorimportance: must knowfreq 40%

basics

~20 s

buildFinished captures live build objects, so it breaks the configuration cache. The Flow API replaces it: you inject FlowScope and FlowProviders, call scope.always(MyFlowAction::class) with serializable Property inputs, and the action runs at build completion in a cache-safe way.

open as a page

When would you use a build-completion hook (buildFinished / FlowAction) versus task finalizedBy? Contrast their semantics.

level: middleimportance: should knowfreq 25%

basics

~20 s

Build-completion hooks run once at the end of the whole build, on success or failure — good for build-global teardown/reporting. finalizedBy attaches a finalizer task to a specific task and runs when that task runs — good for per-task cleanup like collecting reports after a test task.

open as a page

You start an external resource (a Docker container or temp DB) during a build and must guarantee it is torn down even if tasks fail. How would you implement that with a completion hook?

level: middleimportance: should knowfreq 30%

basics

~10 s

Register a completion hook that runs on the failure path too — legacy gradle.buildFinished { ... } or a config-cache-safe FlowScope.always FlowAction. Put the teardown there so it executes whether tasks pass or fail.

open as a page

How would you emit a custom build report or CI notification at the end of every build across a developer team, and where would you register that completion logic?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Register completion logic in an init script (~/.gradle/init.gradle.kts or distributed via an init plugin) so it applies to every build. Use a FlowAction reading buildWorkResult to report pass/fail to CI or a dashboard, config-cache-safe.

open as a page