skip to content

Wiring Extension Values into Tasks

Connecting extension properties to task inputs lazily, so values are read at execution time rather than while the script is being configured. The classic plugin-authoring bug and a favorite interview trap.

on this pageshow

questions

5

Your plugin defines an extension with a `Property<String>` called `greeting`. How do you connect that extension value to a task's input property so the task uses whatever the user sets in the build script?

level: juniorimportance: must knowfreq 60%

answer

  1. set(provider) not set(value)
  2. extension applied before user DSL block
  3. Property is a Provider
  4. never .get() at configuration time
  5. value chain stays live

basics

~10 s

Use task.greeting.set(extension.greeting). This wires the task's Property to the extension's Property lazily, so the value is read later (at execution), not while configuring.

solid answer

~40 s

Both the extension and the task expose a `Property<String>`. In the plugin's `apply` method you register the task and call `task.configure { greeting.set(extension.greeting) }`. Passing the *provider* (the extension's `Property` is itself a `Provider`) instead of `extension.greeting.get()` keeps the wiring lazy: Gradle defers resolving the value until the task actually runs (or until `get()` is called). This matters because at configuration time the user may not have set the extension value yet — the build script block that configures the extension can run after your plugin's `apply`. Calling `.get()` during configuration captures whatever value exists at that instant (often the default or empty), so the user's later assignment is lost. Wiring provider-to-property preserves the value chain end to end.

code

kotlin · 17 lines
kotlin
abstract class GreetExtension {
    abstract val greeting: Property<String>
}

abstract class GreetTask : DefaultTask() {
    @get:Input abstract val greeting: Property<String>
    @TaskAction fun run() = println(greeting.get())
}

class GreetPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val ext = project.extensions.create("greet", GreetExtension::class.java)
        project.tasks.register("greet", GreetTask::class.java) {
            it.greeting.set(ext.greeting) // lazy wiring, no .get()
        }
    }
}

go deeper

for a junior

Know the literal call task.prop.set(extension.prop) and that you must not call .get() while configuring.

for a middle

Explain the apply-vs-user-DSL ordering and why the provider must stay unresolved.

for a senior

Discuss where the wiring lives, that register keeps it lazy, and how the value chain resolves at execution.

for a principal

Frame provider wiring as the contract that makes plugins configuration-cache and ordering safe across a large build.

## The problem this solves A Gradle plugin typically does two things in `apply(project)`: it creates an **extension** (the DSL block users write into, e.g. `myPlugin { greeting = "hi" }`) and it **registers tasks** that do the work. The plugin's `apply` runs *early*, during the project's configuration. But the user's `myPlugin { ... }` block runs *after* `apply`. So at the moment your plugin code runs, the extension still holds its default value. If you read the extension eagerly — `task.greeting = extension.greeting.get()` — you snapshot the default, and the user's later assignment never reaches the task. ## Lazy wiring with Provider/Property Gradle's solution is the **lazy configuration API**: `Provider<T>` (a deferred value you can read later) and `Property<T>` (a `Provider` you can also write to). The key move is to **connect the task's `Property` to the extension's `Property`** rather than copying a resolved value: ```kotlin task.greeting.set(extension.greeting) ``` Here `extension.greeting` is a `Property<String>`, which *is a* `Provider<String>`. `set(Provider)` establishes a live link: when something later calls `task.greeting.get()` (Gradle does this at execution, when it reads the task's `@Input`), it walks the chain back to the extension and reads *its current* value — which by then includes the user's assignment. ## Why not `.get()` during configuration `get()` forces the value *now*. During `apply`, "now" is before the user DSL runs. The fix is never to call `get()` in plugin/configuration code; pass the provider through untouched. ## Where the wiring lives The wiring belongs in the plugin, inside the task-registration callback, so it's set up exactly once per task: ```kotlin val ext = project.extensions.create("myPlugin", MyExtension::class.java) project.tasks.register("greet", GreetTask::class.java) { it.greeting.set(ext.greeting) } ``` Because `register` is itself lazy, the configuration block only runs if the task is actually needed — and even then the value resolution is deferred further to execution.

  • Why does passing `extension.greeting` work where `extension.greeting.get()` would fail?
    `extension.greeting` is a `Provider`, so `set` keeps a live link resolved later; `.get()` resolves immediately during configuration, before the user's DSL block has set the value.
  • Where in the plugin should this wiring go?
    Inside the `tasks.register(...)` configuration callback, so it's established once per task and stays lazy.

Wiring providers is like subscribing to a magazine versus photocopying one issue: set(provider) keeps you subscribed (you get whatever the final value is), while .get() photocopies whatever happens to be on the stand right now.

saying these in an interview costs you the question

  • Saying you should call `.get()` and assign the plain value during `apply`.
  • Believing the user's DSL block runs before the plugin's `apply` method.

context

open as a page

A teammate's plugin reads `extension.outputDir.get()` inside the plugin's `apply` method to configure a task, and reports that the value users set in the build script is being ignored. Diagnose the bug and fix it.

level: middleimportance: must knowfreq 55%

basics

~10 s

.get() resolves the value during configuration, before the user's DSL block runs, so it captures the default. Fix it by wiring the provider lazily: task.outputDir.set(extension.outputDir) with no .get().

open as a page

How do you wire collection-typed and file-system extension values (e.g. `ListProperty<String>`, `DirectoryProperty`) into a task, and what wiring methods differ from a plain `Property`?

level: middleimportance: should knowfreq 40%

basics

~10 s

Wire them the same lazy way with set: task.tags.set(extension.tags) for ListProperty, task.outDir.set(extension.outDir) for DirectoryProperty. Collections also offer add/addAll to append lazily instead of replacing.

open as a page

Why is wiring extension providers into tasks (rather than capturing values via closures or eager reads) essential for configuration-cache compatibility and reliable up-to-date checks?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Provider wiring keeps task inputs as serializable, declared lazy values resolved at execution. Closures over the project/extension capture live objects the configuration cache can't serialize, and eager reads bypass input tracking, breaking up-to-date checks.

open as a page

When wiring extension values into tasks across projects or when an extension value depends on another task's output, how do you keep the wiring lazy and correct — and what does `finalizeValueOnRead` add?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Keep wiring providers, including ones backed by task outputs — task.input.set(otherTask.flatMap { it.output }) — so Gradle infers task dependencies automatically. finalizeValueOnRead locks a property's value on first read to catch late mutations and avoid inconsistent reads.

open as a page