Build Lifecycle
The build lifecycle is the sequence of phases Gradle executes to turn your build scripts into completed work, from initializing the build environment to configuring projects and finally executing tasks.
Build Phases
A build has three distinct phases. Gradle runs these phases in order:
| Phase 1. Initialization | Phase 2. Configuration | Phase 3. Execution |
|---|---|---|
Detects projects and included builds |
Configures projects and builds a task graph |
Schedules and executes the selected tasks |
Phase 1. Initialization
In the initialization phase, Gradle detects the set of projects (root and subprojects) and included builds participating in the build.
Gradle first runs any init scripts, then evaluates the settings file, settings.gradle(.kts), and instantiates a Settings object.
Then, Gradle instantiates Project object instances for each project included in the build (using includeBuild() or include() in the settings file).
Phase 2. Configuration
In the configuration phase, Gradle registers tasks and other properties to the projects found by the initialization phase.
Gradle evaluates the build files of every participating project, then constructs the task graph by analyzing the input and output dependencies of tasks.
| When the Configuration Cache is enabled, the configuration phase also includes a serialization sub-phase, where Gradle stores the task graph while simultaneously precomputing some of the task state and resolving dependencies. This is work that would otherwise be deferred to execution. See Configuration Cache - How It Works for more details. |
Phase 3. Execution
In the execution phase, Gradle runs the selected tasks.
Gradle uses the task execution graph generated by the configuration phase to determine which tasks to execute and in what order.
Gradle can execute tasks that don’t depend on each other, in the same project, in parallel.
The Phases in Build Scripts
The following example shows which parts of settings and build files correspond to various build phases:
rootProject.name = "basic"
println("This is executed during the initialization phase.")
// Configuration Phase
println("This is executed during the configuration phase.")
tasks.register("configured") {
println("This is also executed during the configuration phase.")
}
// Execution Phase
tasks.register("test") {
doLast {
println("This is executed during the execution phase.")
}
}
// Configurations AND Execution Phase
tasks.register("testBoth") {
println("This is executed during the configuration phase as well.")
doFirst {
println("This is executed first during the execution phase.")
}
doLast {
println("This is executed last during the execution phase.")
}
}
rootProject.name = 'basic'
println 'This is executed during the initialization phase.'
// Configuration Phase
println 'This is executed during the configuration phase.'
tasks.register('configured') {
println 'This is also executed during the configuration phase.'
}
// Execution Phase
tasks.register('test') {
doLast {
println 'This is executed during the execution phase.'
}
}
// Configurations AND Execution Phase
tasks.register('testBoth') {
println 'This is executed during the configuration phase as well.'
doFirst {
println 'This is executed first during the execution phase.'
}
doLast {
println 'This is executed last during the execution phase.'
}
}
The following command executes the test and testBoth tasks specified above:
$ ./gradlew test testBoth
This is executed during the initialization phase.
> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well.
> Task :test
This is executed during the execution phase.
> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.
BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
$ ./gradlew test testBoth
This is executed during the initialization phase.
> Configure project :
This is executed during the configuration phase.
This is executed during the configuration phase as well.
> Task :test
This is executed during the execution phase.
> Task :testBoth
This is executed first during the execution phase.
This is executed last during the execution phase.
BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
Because Gradle only configures requested tasks and their prerequisites, the configured task never configures.
Build Lifecycle Timeline
Understanding the precise sequence in which Gradle evaluates scripts and triggers hooks is the key to mastering build logic. This timeline provides a comprehensive overview of the Gradle build flow when the Configuration Cache and Isolated Projects are disabled:
| Phase | Seq. | Hook / Block | Location | Purpose |
|---|---|---|---|---|
Initialization |
1 |
|
Global environment setup (e.g., enterprise repos). |
|
2 |
init script or |
Runs before the settings file is even parsed. |
||
3 |
|
|
Must be first. Defines plugin repos and version rules. |
|
4 |
|
|
Applies plugins to the |
|
5 |
Settings Script Body |
|
Evaluates |
|
6 |
init script or |
Settings are fully processed; Project objects are created. |
||
7 |
init script or |
All project instances exist but aren’t configured yet. |
||
Configuration |
8 |
init script or |
Not recommended. Fires immediately before each project starts evaluating. |
|
9 |
init script or parent project’s |
Runs before a specific project’s build script is evaluated. |
||
10 |
|
|
Sets the classpath for the build script itself. |
|
11 |
|
|
Applies plugins and adds DSL/Tasks. |
|
12 |
Build Script Body |
|
Registers tasks ( |
|
13 |
|
Not recommended. Runs after a specific project is evaluated; useful for reacting to another project’s configuration. |
||
14 |
init script or root |
Fires immediately after each project finishes evaluating. |
||
15 |
init script or root |
All project scripts have run; final chance to tweak the graph. |
||
16 |
Task Graph Construction |
Internal ( |
Gradle calculates the DAG based on requested tasks. |
|
Execution |
17a |
Task Input Snapshotting |
Internal ( |
Evaluated per task to determine whether it should be skipped or executed. |
17b |
Internal ( |
Constructs a dependency graph based on declared dependencies. |
||
17c |
Internal ( |
Dependencies are downloaded from repositories. |
||
18 |
Task Execution |
Console / CLI ( |
Tasks run. |
* Dependency Graph Resolution and Artifact Resolution are normally deferred until the Execution Phase.
However, any build logic that eagerly accesses a configuration’s resolved state, such as calling configuration.files,
configuration.resolvedConfiguration, or iterating a configuration directly, will trigger resolution immediately at that point in the build lifecycle.
| When the Configuration Cache is enabled, Dependency Graph Resolution and Artifact Resolution must be completed during the Configuration Phase rather than at execution time, since they are required to fully describe the task graph before the result can be serialized. You can refer to Configuration Cache - How It Works to learn more. |
Task Graphs
As a build author, you write build logic by defining tasks and declaring how they depend on one another. Gradle uses this information to construct a task graph during the configuration phase that models the relationships between these tasks.
For example, if your project includes tasks such as build, assemble, and createDocs, and you declare that assemble depends on build, and createDocs depends on assemble, Gradle constructs a graph with this order: build → assemble → createDocs.
Gradle builds the task graph before executing any task(s).
Across all projects in the build, tasks form a Directed Acyclic Graph (DAG).
This diagram shows two example task graphs, one abstract and the other concrete, with dependencies between tasks represented as arrows:
Hooks into the Gradle Build Lifecycle
Gradle exposes several APIs that let you listen to or react to key points in the build lifecycle.
Some APIs let you hook into those phases in init scripts, settings.gradle(.kts) or build.gradle(.kts) to customize or inspect the build as Gradle configures it.
These hooks let you:
-
react to the structure of the build (root + subprojects),
-
configure all projects before their own build scripts are evaluated,
-
debug or collect information during configuration.
Lifecycle API
The GradleLifecycle API, accessible the gradle object, can be used to register actions to be executed at certain points in the build lifecycle:
buildStarted -> DEPRECATED in Gradle 6
└─ beforeSettings
└── settingsEvaluated
└── projectsLoaded
├── beforeProject
├── afterProject
└── projectsEvaluated
└── buildFinished -> DEPRECATED in Gradle 7
Here is a quick overview:
| # | Hook | Best for |
|---|---|---|
Early wiring from an init script: global logging/metrics, tweaking |
||
Adjusting repositories, enabling build scans, reading environment variables, or changing build layout. |
||
Applying configuration to every project (for example, adding a plugin to all subprojects). |
||
Applying defaults, logging project configuration start. |
||
Validating configuration or detecting missing plugins. |
||
Performing cross-project validation, configuration summaries, or creating aggregated tasks. |
beforeSettings
Where: init script.
When: By the time settings.gradle(.kts) is executing, beforeSettings has already fired.
// 1. beforeSettings: tweak start parameters / log early info
// before the build settings have been loaded and evaluated.
gradle.beforeSettings {
println("[beforeSettings] gradleUserHome = ${gradle.gradleUserHomeDir}")
// Example: default to --parallel if an env var is set
if (System.getenv("CI") == "true") {
println("[beforeSettings] Enabling parallel execution on CI")
gradle.startParameter.isParallelProjectExecutionEnabled = true
} else {
println("[beforeSettings] Disabling parallel execution, not on CI")
gradle.startParameter.isParallelProjectExecutionEnabled = false
}
}
// 1. beforeSettings: tweak start parameters / log early info
// before the build settings have been loaded and evaluated.
gradle.beforeSettings {
println("[beforeSettings] gradleUserHome = ${gradle.gradleUserHomeDir}")
// Example: default to --parallel if an env var is set
if (System.getenv("CI") == "true") {
println("[beforeSettings] Enabling parallel execution on CI")
gradle.startParameter.parallelProjectExecutionEnabled = true
} else {
println("[beforeSettings] Disabling parallel execution, not on CI")
gradle.startParameter.parallelProjectExecutionEnabled = false
}
println("")
}
settingsEvaluated
Where: settings.gradle(.kts).
When: After the settings file finishes evaluating.
// 2. settingsEvaluated: adjust build layout / repositories / scan config
// when the build settings have been loaded and evaluated.
gradle.settingsEvaluated {
println("[settingsEvaluated] rootProject = ${rootProject.name}")
// Example: enforce a company-wide pluginManagement repo
pluginManagement.repositories.apply {
println("[settingsEvaluated] Ensuring company plugin repo is configured")
mavenCentral()
}
}
// 2. settingsEvaluated: adjust build layout / repositories / scan config
// when the build settings have been loaded and evaluated.
gradle.settingsEvaluated { settings ->
println("[settingsEvaluated] rootProject = ${settings.rootProject.name}")
// Example: enforce a company-wide pluginManagement repo
settings.pluginManagement.repositories.with {
println("[settingsEvaluated] Ensuring company plugin repo is configured")
mavenCentral()
}
println("")
}
projectsLoaded
Where: settings.gradle(.kts).
When: All projects have been discovered but not yet configured.
// 3. projectsLoaded: we know the full project graph, but nothing configured yet
// to be called when the projects for the build have been created from the settings.
gradle.projectsLoaded {
println("[projectsLoaded] Projects discovered: " + rootProject.allprojects.joinToString { it.name })
// Example: Add a custom property (using the extra properties extension)
allprojects {
println("[projectsLoaded] Setting extra property on ${name}")
extensions.extraProperties["isInitScriptConfigured"] = true
}
}
// 3. projectsLoaded: we know the full project graph, but nothing configured yet
// to be called when the projects for the build have been created from the settings.
gradle.projectsLoaded {
println "[projectsLoaded] Projects discovered: " + rootProject.allprojects.collect { it.name }.join(', ')
// Example: Add a custom property (using the extra properties extension)
allprojects {
println("[projectsLoaded] Setting extra property on ${name}")
extensions.extraProperties["isInitScriptConfigured"] = true
}
}
beforeProject
Where: build.gradle(.kts) or anywhere you have access to the Gradle instance.
When: For each project as its build script is evaluated.
// to be called immediately before a project is evaluated.
gradle.lifecycle.beforeProject {
println("[lifecycle.beforeProject] Started configuring ${path}")
}
// 4. beforeProject: runs before each build.gradle(.kts) is evaluated
// to be called immediately before a project is evaluated.
gradle.beforeProject {
println("[beforeProject] Started configuring ${path}")
println("[beforeProject] Setup a global build directory for ${name}")
layout.buildDirectory.set(
layout.projectDirectory.dir("build")
)
}
// to be called immediately before a project is evaluated.
gradle.lifecycle.beforeProject {
println("[lifecycle.beforeProject] Started configuring ${path}")
}
// 4. beforeProject: runs before each build.gradle(.kts) is evaluated
// to be called immediately before a project is evaluated.
gradle.beforeProject { project ->
println("[beforeProject] Started configuring ${project.path}")
println("[beforeProject] Setup a global build directory for ${project.name}")
project.layout.buildDirectory.set(
project.layout.projectDirectory.dir("build")
)
}
afterProject
Where: build.gradle(.kts) or anywhere you have access to the Gradle instance.
When: For each project as its build script is evaluated.
// 5. afterProject: runs after each build.gradle(.kts) is evaluated
// to be called immediately after a project is evaluated.
gradle.afterProject {
println("[afterProject] Finished configuring ${path}")
// Example: apply the Java plugin to all projects that don’t have any plugin yet
if (plugins.hasPlugin("java")) {
println("[afterProject] ${path} already has the java plugin")
} else {
println("[afterProject] Applying java plugin to ${path}")
apply(plugin = "java")
}
}
// to be called immediately after a project is evaluated.
gradle.lifecycle.afterProject {
println("[lifecycle.afterProject] Finished configuring ${path}")
}
// 5. afterProject: runs after each build.gradle(.kts) is evaluated
// to be called immediately after a project is evaluated.
gradle.afterProject { project ->
println("[afterProject] Finished configuring ${project.path}")
// Example: apply the Java plugin to all projects that don’t have any plugin yet
if (project.plugins.hasPlugin("java")) {
println("[afterProject] ${project.path} already has the java plugin")
} else {
println("[afterProject] Applying java plugin to ${project.path}")
project.apply(plugin: "java")
}
}
// to be called immediately after a project is evaluated.
gradle.lifecycle.afterProject {
println("[lifecycle.afterProject] Finished configuring ${path}")
}
projectsEvaluated
Where: build.gradle(.kts) or anywhere you have access to the Gradle instance.
When: After all projects have been evaluated (i.e., all build.gradle(.kts) files are read and configuration is complete), but before task graph is finalized and before execution starts.
// 6. projectsEvaluated: all projects are fully configured, safe for cross-project checks
// to be called when all projects for the build have been evaluated.
gradle.projectsEvaluated {
println("[projectsEvaluated] All projects evaluated")
// Example: globally configure the java plugin
allprojects {
extensions.findByType<JavaPluginExtension>()?.let { javaExtension ->
if (javaExtension.toolchain.languageVersion.isPresent) {
println("[projectsEvaluated] ${path} uses Java plugin with toolchain ${javaExtension.toolchain.displayName}")
} else {
println("[projectsEvaluated] WARNING: ${path} uses Java plugin but no toolchain is configured, setting Java 17")
javaExtension.toolchain.languageVersion.set(JavaLanguageVersion.of(17))
}
}
}
}
// 6. projectsEvaluated: all projects are fully configured, safe for cross-project checks
// to be called when all projects for the build have been evaluated.
gradle.projectsEvaluated {
println("[projectsEvaluated] All projects evaluated")
// Example: globally configure the java plugin
allprojects { project ->
def javaExtension = project.extensions.findByType(JavaPluginExtension)
if(javaExtension) {
if (javaExtension.toolchain.languageVersion.orNull != null) {
println("[projectsEvaluated] ${path} uses Java plugin with toolchain ${javaExtension.toolchain.displayName}")
} else {
println("[projectsEvaluated] WARNING: ${path} uses Java plugin but no toolchain is configured, setting Java 17")
javaExtension.toolchain.languageVersion.set(JavaLanguageVersion.of(17))
}
}
}
}
Build Listeners
Listeners are interfaces you implement to react to build lifecycle events.
Many BuildListeners are deprecated and/or are not Configuration Cache friendly, use them with caution:
| Interface | What it observes | Works with CC? |
|---|---|---|
Fires when projects are loaded/evaluated. |
❌ |
|
Fires when the execution of an operation progresses. |
✅ |
|
(Partially Deprecated) |
Fires when a task has started or completed its action. |
❌ |
(Deprecated) |
Fires before and after each task executes. |
❌ |
(Deprecated) |
Fires when the task execution graph has been populated. |
❌ |
Fires when an operation completes. |
✅ |
|
Fires before and after dependency resolution. |
❌ |
|
Fires before and after each project is evaluated. |
❌ |
|
Fires at different times during test execution. |
❌ |
You implement a listener interface and typically register it on the gradle object.
The TaskExecutionGraphListener fires once after the task graph has been calculated, but before any task executes.
This example uses graph.allTasks to inspect what Gradle plans to run, and graph.hasTask() to conditionally adjust behaviour:
gradle.taskGraph.addTaskExecutionGraphListener { graph ->
val tasks = graph.allTasks.joinToString("\n ") { it.path }
println("[TaskExecutionGraphListener] Graph is ready. Tasks to execute:\n $tasks")
if (graph.hasTask(":skippedTask")) {
println("[TaskExecutionGraphListener] :skippedTask is in the graph (it will still be skipped via onlyIf)")
}
}
tasks.register("hello") {
group = "demo"
description = "Prints a greeting. Always runs — observe TaskExecutionListener output."
doLast {
println("Hello from the :hello task!")
}
}
tasks.register("upToDateTask") {
group = "demo"
description = "Writes a file. Runs once, then reports UP-TO-DATE on subsequent runs."
val outputFile = layout.buildDirectory.file("up-to-date-output.txt")
outputs.file(outputFile)
doLast {
outputFile.get().asFile.writeText("generated")
println("upToDateTask: file written.")
}
}
tasks.register("skippedTask") {
group = "demo"
description = "Always skipped via onlyIf — observe SKIPPED in TaskExecutionListener."
onlyIf { false }
doLast {
println("This never prints.")
}
}
gradle.taskGraph.addTaskExecutionGraphListener { graph ->
def tasks = graph.allTasks.collect { it.path }.join("\n ")
println("[TaskExecutionGraphListener] Graph is ready. Tasks to execute:\n ${tasks}")
if (graph.hasTask(":skippedTask")) {
println("[TaskExecutionGraphListener] :skippedTask is in the graph (it will still be skipped via onlyIf)")
}
}
tasks.register("hello") {
group = "demo"
description = "Prints a greeting. Always runs."
doLast {
println("Hello from the :hello task!")
}
}
tasks.register("upToDateTask") {
group = "demo"
description = "Writes a file. Runs once, then reports UP-TO-DATE on subsequent runs."
def outputFile = layout.buildDirectory.file("up-to-date-output.txt")
outputs.file(outputFile)
doLast {
outputFile.get().asFile.text = "generated"
println("upToDateTask: file written.")
}
}
tasks.register("skippedTask") {
group = "demo"
description = "Always skipped via onlyIf."
onlyIf { false }
doLast {
println("This never prints.")
}
}
Note that tasks skipped via onlyIf still appear in the graph; skipping is evaluated later, per task, during execution:
[TaskExecutionGraphListener] Graph is ready. Tasks to execute:
:hello
:skippedTask
:upToDateTask
:runAll
[TaskExecutionGraphListener] :skippedTask is in the graph (it will still be skipped via onlyIf)
> Task :hello
Hello from the :hello task!
> Task :skippedTask SKIPPED
> Task :upToDateTask
upToDateTask: file written.
> Task :runAll
BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
Build Services
Build Services are used to share state or resources across tasks and across the build lifecycle.
This example implements OperationCompletionListener directly on the service to count tasks as they complete, and uses close() as an end-of-build hook to print the final summary, whether the build succeeded or failed:
abstract class BuildDurationService
: BuildService<BuildServiceParameters.None>, AutoCloseable {
private val startTime = System.currentTimeMillis()
// Called once at the end of the build — reliable teardown hook
override fun close() {
val elapsed = System.currentTimeMillis() - startTime
println("─────────────────────────────────────")
println(" Build duration : ${elapsed}ms")
println("─────────────────────────────────────")
}
}
val buildDurationService = gradle.sharedServices.registerIfAbsent("buildDuration", BuildDurationService::class) {
maxParallelUsages = 1
}
tasks.register("taskA") {
group = "demo"
usesService(buildDurationService)
val service = buildDurationService
doLast {
service.get()
println("taskA running...")
Thread.sleep(200)
}
}
tasks.register("taskB") {
group = "demo"
doLast {
println("taskB running...")
Thread.sleep(300)
}
}
abstract class BuildDurationService
implements BuildService<BuildServiceParameters.None>, AutoCloseable {
private final long startTime = System.currentTimeMillis()
// Called once at the end of the build — reliable teardown hook
@Override
void close() {
def elapsed = System.currentTimeMillis() - startTime
println("─────────────────────────────────────")
println(" Build duration : ${elapsed}ms")
println("─────────────────────────────────────")
}
}
def buildDurationService = gradle.sharedServices.registerIfAbsent("buildDuration", BuildDurationService) {
maxParallelUsages = 1
}
tasks.register("taskA") {
group = "demo"
usesService(buildDurationService)
def service = buildDurationService
doLast {
service.get()
println("taskA running...")
Thread.sleep(200)
}
}
tasks.register("taskB") {
group = "demo"
doLast {
println("taskB running...")
Thread.sleep(300)
}
}
The hooks are used to calculate the duration of the build:
> Task :taskA
taskA running...
> Task :taskB
taskB running...
> Task :runAll
─────────────────────────────────────
Build duration : 510ms
─────────────────────────────────────
BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
Flow Actions Incubating
FlowAction is a newer API (introduced in Gradle 7.6) that lets you run logic in response to build lifecycle events, most commonly, when the build finishes.
It was introduced as a Configuration Cache and Isolated Projects compatible alternative to BuildListener and other deprecated lifecycle listeners.
To use a Flow Action, you define a class implementing FlowAction<T> and a matching Parameters interface that declares the inputs your action needs.
You then register it inside a Plugin using FlowScope, which is injected by Gradle alongside FlowProviders:
abstract class PrintBuildResultPlugin : Plugin<Project> {
@get:Inject
abstract val flowScope: FlowScope
@get:Inject
abstract val flowProviders: FlowProviders
override fun apply(target: Project) {
flowScope.always(BuildResultPrinter::class.java) {
parameters.buildResult.set(flowProviders.buildWorkResult)
}
}
}
abstract class BuildResultPrinter : FlowAction<BuildResultPrinter.Parameters> {
interface Parameters : FlowParameters {
@get:Input
val buildResult: Property<BuildWorkResult>
}
override fun execute(parameters: Parameters) {
val result = parameters.buildResult.get()
if (result.failure.isPresent) {
println("Build failed: ${result.failure.get().message}")
} else {
println("Build succeeded")
}
}
}
abstract class PrintBuildResultPlugin implements Plugin<Project> {
@Inject
abstract FlowScope getFlowScope()
@Inject
abstract FlowProviders getFlowProviders()
@Override
void apply(Project target) {
flowScope.always(BuildResultPrinter) {
parameters.buildResult.set(flowProviders.buildWorkResult)
}
}
}
abstract class BuildResultPrinter implements FlowAction<BuildResultPrinter.Parameters> {
interface Parameters extends FlowParameters {
@Input
Property<BuildWorkResult> getBuildResult()
}
@Override
void execute(Parameters parameters) {
def result = parameters.buildResult.get()
if (result.failure.isPresent()) {
println "Build failed: ${result.failure.get().message}"
} else {
println "Build succeeded"
}
}
}
As expected, we see the build finish success message:
Build succeeded
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed