Understanding the Gradle ValueSource API
Is your Gradle build time getting longer as your project grows? If you find yourself waiting a long time every time you sync your project in the IDE, it may be worth examining the code that runs during the configuration phase.
In this article, we will look at why the ValueSource API, introduced in Gradle 6.1, is needed and how it integrates with the Configuration Cache.
Understanding the Three Phases of a Gradle Build
First, a Gradle build is broadly divided into three phases:
- Initialization Phase: Gradle determines which projects are part of the build by evaluating the
settings.gradle(.kts)file, and creates aProjectinstance for each - Configuration Phase: The phase where build scripts are evaluated and the task graph is constructed
- Execution Phase: The phase where tasks are actually executed
For the purposes of this article, we focus on the distinction between the configuration and execution phases, since that is where the ValueSource API plays its role. The problem is that the configuration phase runs on every build. This includes when you sync your project in the IDE or even when you simply run ./gradlew tasks.
A Common Mistake
Many developers write code like this in their build scripts:
tasks.register('generateBuildInfo') {
def gitBranch = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim()
def gitCommit = 'git rev-parse --short HEAD'.execute().text.trim()
doLast {
println "Branch: ${gitBranch}"
println "Commit: ${gitCommit}"
}
}
The problem with this code is that it executes at task registration time. The Git commands run in the task configuration block, not in the doLast block. This means they execute every time you sync your project, build, or even run a help task. On top of that, it prevents the use of the Configuration Cache.
For these reasons, I would like to introduce the ValueSource API.
ValueSource API
ValueSource is an interface for accessing external sources, introduced in Gradle 6.1.
public interface ValueSource<T, P extends ValueSourceParameters> {
@Nullable
T obtain();
}
ValueSource provides controlled execution where file reads and environment variable access inside obtain() are not recorded as build inputs, and Gradle decides when to call obtain(). It also integrates naturally with the Provider pattern for lazy evaluation.

Normally, rather than Gradle automatically invalidating the cache whenever a file or environment variable changes, the cache is only invalidated when the actual value returned by the obtain() method changes. In other words, even if an environment variable changes, the cache is preserved if the return value of obtain() remains the same.
Basic Implementation
Let us implement a ValueSource that retrieves Git branch information, a commonly used example.
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.process.ExecOperations
import javax.inject.Inject
import java.io.ByteArrayOutputStream
abstract class GitBranchValueSource : ValueSource<String, ValueSourceParameters.None> {
@get:Inject
abstract val execOperations: ExecOperations
override fun obtain(): String? {
val output = ByteArrayOutputStream()
return try {
execOperations.exec {
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
standardOutput = output
isIgnoreExitValue = true
}
output.toString().trim().takeIf { it.isNotEmpty() }
} catch (e: Exception) {
null
}
}
}
Using It in a Task
tasks.register("printBuildInfo") {
val gitBranch = providers.of(GitBranchValueSource::class.java) {}
// Register as an input to enable up-to-date checks
inputs.property("branch", gitBranch)
doLast {
// obtain() is called for the first time at this point
println("Current branch: ${gitBranch.get()}")
}
}
What About the Gradle 7.5+ Provider API?
Additionally, providers.exec() was added starting with Gradle 7.5, but it has different characteristics from ValueSource.
// For simple cases - using providers.exec()
val gitBranch = providers.exec {
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
}.standardOutput.asText
// Using it in a task
tasks.register("showBranch") {
doLast {
println(gitBranch.get())
}
}
| Feature | ValueSource (6.1+) | providers.exec() (7.5+) |
|---|---|---|
| Introduced in | Gradle 6.1 | Gradle 7.5 |
| Complex logic | Supported via obtain() method | Not supported - captures output only |
| Error handling | Fine-grained control with try-catch | Limited |
| Parameters | ValueSourceParameters supported | Not supported |
| Cache control | Return value-based caching | Simple caching |
For simple command execution or using output values as-is, providers.exec() seems sufficient. However, for cases requiring complex logic, important error handling, or parameterization, using ValueSource is more appropriate.
ValueSource with Parameters
For more complex cases, you can accept parameters by implementing the ValueSourceParameters marker interface, as shown in the following snippet.
interface GitCommandParameters : ValueSourceParameters {
val gitArgs: ListProperty<String>
val workingDir: DirectoryProperty
}
abstract class GitCommandValueSource : ValueSource<String, GitCommandParameters> {
@get:Inject
abstract val execOperations: ExecOperations
override fun obtain(): String? {
val output = ByteArrayOutputStream()
return try {
execOperations.exec {
workingDirectory = parameters.workingDir.get().asFile
commandLine("git", *parameters.gitArgs.get().toTypedArray())
standardOutput = output
isIgnoreExitValue = true
}
output.toString().trim().takeIf { it.isNotEmpty() }
} catch (e: Exception) {
null
}
}
}
// Usage example
val commitHash = providers.of(GitCommandValueSource::class.java) {
parameters {
gitArgs.set(listOf("rev-parse", "--short", "HEAD"))
workingDir.set(layout.projectDirectory)
}
}
Considerations

One important point to note is that, as stated in the official Gradle documentation, even when the Configuration Cache is enabled, the obtain() method is called on every build to validate cache freshness. Therefore, you should avoid network requests or heavy operations in obtain() and use it only for fast operations like reading local files or retrieving Git information.
In other words, obtain() is called on every build to compare the return value with the previous cache. If the value has changed, the Configuration Cache is invalidated; if it is the same, the Configuration Cache is reused. This means that making network requests that take 5 seconds or more, as in the code below, can be problematic:
abstract class SlowValueSource : ValueSource<String, ValueSourceParameters.None> {
override fun obtain(): String {
// Network request taking 5+ seconds
return fetchFromRemoteServer() // Takes 5 seconds on every build
}
}
Another important consideration when using ValueSource is to never call .get() directly during the configuration phase.
// Problem pattern
val gitBranch = providers.of(GitBranchValueSource::class.java) {}.get()
For example, if the value main is fixed in the Configuration Cache at that point, and the Git branch is later changed from main to feature/**, the Configuration Cache will still use main, which can produce incorrect build results.
The design intent of ValueSource is the flow: build starts -> obtain() called on every build -> value comparison -> cache validity check (reuse cache if same, invalidate cache if different). However, calling .get() during the configuration phase leads to this flow instead: configuration phase -> .get() -> value fixed -> stored in cache.
The core of the Provider pattern is lazy evaluation, and to ensure that obtain() is only called when the task actually executes, you should use the following pattern:
// Correct pattern
tasks.register("deploy") {
val versionProvider = providers.of(VersionValueSource::class.java) {}
inputs.property("version", versionProvider) // Pass the Provider as-is
doLast {
// Evaluated only during the execution phase
println("Version: ${versionProvider.get()}")
}
}
Summary
The ValueSource API follows the Provider pattern while providing specialized functionality for accessing external sources, and it is compatible with the Configuration Cache. This has become particularly important since the Configuration Cache became a stable feature starting with Gradle 8.1.

Avoid slow operations in obtain(), and use this API when you need to include Git information (branch, commit hash) in your build, read environment variables to determine build settings, or detect CI/CD environments to modify build behavior. For passing internal project values or performing simple computations, the standard Provider API should be sufficient.
As a final reminder, calling .get() directly during the configuration phase fixes the value and loses the benefits of the Configuration Cache. Always pass values as Providers to tasks and resolve them during the execution phase.
References
#gradle #build #configuration-cache #valuesource