Why is the Flow API (FlowScope/FlowAction) preferred over gradle.buildFinished, and how do you wire up a completion action with it?
answer
- config cache forbids live-object closures
- FlowAction + FlowParameters (Property inputs)
- FlowScope.always / whenever
- FlowProviders.getBuildWorkResult()
- BuildWorkResult.failure: Optional<Throwable>
basics
~20 sbuildFinished 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.
solid answer
~40 sThe configuration cache serializes the configured build so it can be reused, which forbids hooks that hold references to live model objects — exactly what `buildFinished` closures do. The **Flow API** (incubating, stabilizing through Gradle 8.x) fixes this. You define a `FlowAction<Params>` with a `Parameters` interface of serializable `Property`/`Provider` inputs, then register it via an injected `FlowScope`: `flowScope.always(MyAction::class) { parameters.x.set(...) }`. Inputs you need at completion time — like the build result — come from `FlowProviders.getBuildWorkResult()`, a `Provider<BuildWorkResult>` exposing the failure. Because every input is a declared, serializable Provider, the whole thing survives the configuration cache. `always` runs on success and failure; `whenever` lets you gate it. This is the supported path for cleanup and reporting in modern Gradle.
code
kotlin · 13 linesabstract class CleanupFlowAction : FlowAction<CleanupFlowAction.Params> {
interface Params : FlowParameters {
@get:Input val failed: Property<Boolean>
}
override fun execute(parameters: Params) {
println(if (parameters.failed.get()) "cleanup after failure" else "cleanup")
}
}
// inside a Plugin<Settings>, with injected flowScope + flowProviders:
flowScope.always(CleanupFlowAction::class.java) {
parameters.failed.set(flowProviders.buildWorkResult.map { it.failure.isPresent })
}go deeper
Aware that a newer, configuration-cache-safe replacement for buildFinished exists.
Explain that the config cache forbids live-object closures and name FlowAction/FlowScope as the replacement.
Wire a complete FlowAction: Parameters with Property inputs, FlowScope.always, FlowProviders.getBuildWorkResult, and explain why it serializes.
Drive a fleet-wide migration off buildFinished, set conventions for completion telemetry, and weigh incubating-API risk.
## The problem the Flow API solves The **configuration cache** stores the result of the configuration phase (the task graph and its state) so subsequent builds skip configuration entirely. For this to work, anything that must run later has to be **serializable** — it cannot close over the live `Project`, `Gradle`, `Task`, or arbitrary services. `gradle.buildFinished { ... }` does exactly that: its closure can reference anything in scope. Hence it is reported as a configuration-cache **problem**. ## The Flow API building blocks - **`FlowAction<P : FlowParameters>`** — your unit of work. Implement `execute(parameters: P)`. The `P` is a nested `interface ... : FlowParameters` whose getters return `Property<T>`/`Provider<T>` (managed, serializable). - **`FlowScope`** — injected service used to *register* flow actions. Two entry points: - `always(actionType) { parameters... }` — run unconditionally at build completion. - `whenever(condition) { ... }` — run conditionally. - **`FlowProviders`** — injected service supplying lazy inputs, notably `getBuildWorkResult(): Provider<BuildWorkResult>`. `BuildWorkResult.getFailure(): Optional<Throwable>` is the success/failure signal. You obtain `FlowScope`/`FlowProviders` via `@Inject` in a settings plugin or project plugin. ## Wiring it up ```kotlin abstract class ReportFlowAction : FlowAction<ReportFlowAction.Params> { interface Params : FlowParameters { @get:Input val buildFailed: Property<Boolean> } override fun execute(parameters: Params) { val status = if (parameters.buildFailed.get()) "FAILED" else "OK" println("Build finished: $status") } } class ReportPlugin @Inject constructor( private val flowScope: FlowScope, private val flowProviders: FlowProviders, ) : Plugin<Settings> { override fun apply(target: Settings) { flowScope.always(ReportFlowAction::class.java) { parameters.buildFailed.set( flowProviders.buildWorkResult.map { it.failure.isPresent } ) } } } ``` ## Why this is cache-safe Nothing in the action captures live model state. `buildWorkResult` is a `Provider` resolved at completion; the parameter is a `Property<Boolean>` mapped from it. The serialized form is just the provider chain and parameter values — all replayable. Therefore the build can be configuration-cached and the completion action still runs. ## `always` vs `whenever` `always` mirrors `buildFinished`'s 'run no matter what' semantics. `whenever(spec)` registers conditionally — useful when you only want the action under certain build settings. Both ultimately produce a flow that Gradle schedules at the end of the build. ## Migration guidance For cleanup, telemetry, Slack/CI notifications, or deleting temp artifacts at the end of a build, migrate from `buildFinished` to a `FlowAction` reading `buildWorkResult`. It's more boilerplate but it's the only completion hook that coexists with the configuration cache.
- How does a FlowAction get the build's success/failure?Through FlowProviders.getBuildWorkResult() — a Provider<BuildWorkResult> whose getFailure() returns an Optional<Throwable>; you map it into a serializable parameter.
- What's the difference between always() and whenever()?always() schedules the action unconditionally at completion (like buildFinished); whenever() registers it conditionally based on a spec, so it only fires when the condition holds.
- Where do you obtain FlowScope and FlowProviders?By @Inject constructor injection into a settings or project plugin — they are Gradle services, not directly available as script properties.
buildFinished is like leaving a sticky note that points at objects on your desk — useless once the desk is packed away. A FlowAction is a self-contained, serialized work order: everything it needs is written on the order itself, so it still runs after the desk is in storage.
saying these in an interview costs you the question
- Claiming you can just keep buildFinished and ignore configuration-cache warnings.
- Putting non-serializable references (Project, Task) inside FlowParameters.
- Thinking FlowAction inputs are read eagerly at registration — they are lazy Providers resolved at completion.