skip to content

How do you define a custom task in build.gradle.kts, and what is the difference between tasks.register and tasks.create?

level: middleimportance: should knowfreq 45%

answer

  1. register = lazy, returns TaskProvider
  2. create = eager, returns the Task
  3. Configuration avoidance saves config-phase time
  4. named<T>() to configure existing tasks lazily
  5. doLast/doFirst add actions; dependsOn orders

basics

~10 s

You define a task with tasks.register("name") { ... }, configuring it inside the receiver block. register creates the task lazily (only when needed), while create makes it eagerly every build, which is slower.

solid answer

~40 s

In the Kotlin DSL you register a task with tasks.register("name") { ... } or the typed tasks.register<Copy>("name") { ... }, configuring it inside a receiver block where this is the task. tasks.register returns a TaskProvider and is part of the configuration-avoidance API: the task is created and configured lazily, only if it ends up in the task graph. tasks.create is the older eager API: it instantiates and configures the task immediately during configuration, even if the task never runs, which hurts configuration-time performance on large builds. Best practice is register plus configuration avoidance everywhere (tasks.named<T>("x") to tweak existing tasks). Inside the block you wire inputs/outputs and add actions via doLast { } / doFirst { }; with the typed overload you can set type-specific properties (e.g. a Copy task's from/into).

code

kotlin · 15 lines
kotlin
// Lazy registration with a typed task:
val copyDocs = tasks.register<Copy>("copyDocs") {
    from("src/docs")
    into(layout.buildDirectory.dir("docs"))
}

// Lazy configuration of an existing task + wiring via the provider:
tasks.named("build") {
    dependsOn(copyDocs)
}

// Eager (avoid on large builds): created and configured even if never run
tasks.create("legacyHello") {
    doLast { println("hi") }
}

go deeper

for a junior

Can register a simple task with a doLast action and knows register is preferred.

for a middle

Explains lazy vs eager, TaskProvider, and configuration avoidance; uses named for existing tasks.

for a senior

Connects eager creation to configuration-phase cost on large multi-module builds and wires tasks via providers and inputs/outputs.

for a principal

Sets team conventions enforcing register/named and reasons about build-time scalability and configuration-cache compatibility.

## Defining a task A *task* is a unit of build work. In the Kotlin DSL: ```kotlin tasks.register("hello") { group = "custom" description = "Prints a greeting" doLast { // an action that runs at execution time println("Hello from Gradle") } } ``` - `register("name") { ... }` — the lambda is a **receiver block** where `this` is the task being configured, so you set `group`, `description`, inputs/outputs, and actions directly. - **Typed overload** lets you create a task of a specific type and use its properties: ```kotlin tasks.register<Copy>("copyDocs") { from("src/docs") into(layout.buildDirectory.dir("docs")) } ``` ## register vs create — the key distinction | | `tasks.register(...)` | `tasks.create(...)` | |---|---|---| | Returns | `TaskProvider<T>` | `T` (the task) | | Timing | **Lazy** — created/configured only if needed | **Eager** — created/configured immediately | | Part of | Configuration-avoidance API | Legacy eager API | | Performance | Cheap on large builds | Pays cost every build, even if task never runs | **Configuration avoidance**: Gradle splits a build into a *configuration phase* (evaluate scripts, build the task graph) and an *execution phase* (run the selected tasks). `register` defers a task's creation/configuration until something actually realizes it (it's requested, or a dependency pulls it in). `create` runs that work during the configuration phase unconditionally. On a multi-module build with hundreds of tasks, eager creation noticeably slows every invocation, even `gradle help`. ## Configuring existing tasks lazily To tweak a task a plugin already registered, use the lazy accessor rather than realizing it: ```kotlin tasks.named<Test>("test") { useJUnitPlatform() maxParallelForks = 4 } ``` `named` returns a `TaskProvider` and configures lazily; `getByName`/`tasks.test` (the eager accessor) realize the task immediately and should be avoided in hot paths. ## doLast / doFirst and dependencies - `doLast { }` appends an action run last; `doFirst { }` prepends one. - Order tasks with `dependsOn(...)`, `mustRunAfter(...)`, `finalizedBy(...)`. - For correctness and up-to-date checks, declare `inputs`/`outputs` so Gradle can skip the task when nothing changed. ## Wiring providers Because `register`/`named` return `TaskProvider`, you can wire tasks together without realizing them: ```kotlin val gen = tasks.register("generate") { /* ... */ } tasks.named("compileKotlin") { dependsOn(gen) } ``` ## Rule of thumb Prefer **`register` + `named`** everywhere; reach for `create`/`getByName` only when forced by an old API. This is officially the recommended style for modern Gradle.

  • What does tasks.register return, and why is the return type useful?
    It returns a TaskProvider, a lazy handle to the (possibly not-yet-created) task. You can pass it to dependsOn or wire its outputs without forcing the task to be created, preserving configuration avoidance.
  • When configuring a plugin-provided task like test, why prefer tasks.named over tasks.getByName?
    named configures lazily through a TaskProvider, so the task is only realized if needed; getByName eagerly realizes it during configuration, defeating configuration avoidance.

register is ordering food only when a customer actually asks; create is cooking every dish on the menu at opening whether or not anyone orders it.

saying these in an interview costs you the question

  • Saying register and create are interchangeable with no performance difference
  • Thinking register returns the Task object rather than a TaskProvider
  • Using tasks.create everywhere by habit on a large build
  • Realizing tasks eagerly (getByName) when named would do
  • Not knowing the configuration vs execution phase split

context