Plugins Overview

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Gradle 9.8.1 (the current release on 2026-10-16) with the embedded Kotlin 2.4.10 and Groovy 4.0.33. Every example was run on JDK 26 through the Gradle wrapper on 2026-10-16, on macOS, with the configuration cache enabled (org.gradle.configuration-cache=true in gradle.properties) unless the text says a run turned it off, and says whether the example is configuration cache compatible. The Kotlin DSL is shown first and the Groovy DSL second wherever the syntax differs, and both versions were run. Paths of the scratch workspace are shortened to <work>, and the task lines of long console outputs are trimmed where it is said. Gradle 8.14 was not available here: what differs in it is described from the official documentation in the last sections and is labelled "not run here".

Gradle itself knows almost nothing about Java, Kotlin, applications or publishing. Everything a build can do beyond running tasks in order comes from plugins: a plugin adds tasks, configurations, extensions, conventions and sometimes whole variants of the dependency model to the object it is applied to. Choosing, naming, locating and applying plugins is therefore the first thing every build script does, and this page is about that act. What happens inside a plugin is on Binary Plugins; how to share your own plugins between projects is on Convention Plugins & Sharing Build Logic; how to test and publish them is on Testing & Publishing Plugins. The vocabulary of builds, projects and tasks is on Core Concepts, and the two scripts that mention plugins first are described on Settings & Build Scripts.

Plugins in the three tools

Every build tool of this section has an extension mechanism, and they differ in how much of the tool itself is an extension.

Ant Maven Gradle

Unit of extension

a task or type, loaded from a JAR with taskdef/typedef or an antlib

a plugin made of goals (Mojos), identified by groupId:artifactId:version

a plugin identified by a plugin id, with or without a version

Core functionality

the core tasks in ant.jar (javac, jar, copy)

almost nothing: compiling, testing and packaging are all plugins bound to lifecycle phases

almost nothing: even java and base are plugins, but they ship inside the distribution

How it is requested

<taskdef> or an xmlns: antlib namespace

<plugin> in the POM, or the default bindings of the packaging

the plugins { } block (or, legacy, apply and buildscript)

Where it is found

the class path (-lib, ~/.ant/lib) or a classpath attribute

the plugin repositories (<pluginRepositories>, Central by default)

core: the distribution; others: the plugin repositories of pluginManagement (Plugin Portal by default)

What it contributes

tasks and types

goals, run in lifecycle phases

tasks, extensions, configurations, conventions, variants

The equivalent pages are Ant: Antlibs & Namespaces, Ant: Writing Custom Tasks, Maven: Plugins & Their Configuration, Maven: Extensions and Maven: Writing Plugins; the three philosophies side by side are on Comparing Ant, Maven & Gradle. The most visible difference for a reader coming from Maven is that a Gradle plugin is applied, not bound: applying java does not decide when anything runs, it only makes tasks such as compileJava exist, and the task graph decides the order.

Three sources and three kinds of plugin

Gradle’s documentation distinguishes where a plugin comes from, and separately how a local plugin is written.

Source What it is Example

Core plugin

part of the Gradle distribution, maintained by the Gradle team, always available, versioned with Gradle

java-library, application, maven-publish

Community plugin

published to the Gradle Plugin Portal (or another repository) by anyone, versioned on its own

io.github.ben-manes.versions

Local or custom plugin

written for your own build or organization, kept in buildSrc, an included build or a standalone project

tidepool.stamp later on this page

Kind (of local plugin) Where it lives Typically Remark

Script plugin

a .gradle or .gradle.kts file pulled in with apply(from = ...), or a class written inside a build script

a throw-away local helper

convenient, but the official documentation says script plugins should be avoided

Precompiled script plugin

buildSrc or an included build, as src/main/kotlin/name.gradle.kts or src/main/groovy/name.gradle

a convention plugin

written in the build DSL, the plugin id comes from the file name

Binary plugin

a class implementing Plugin<Project> in a standalone project, compiled and packaged as a JAR

a shared, published plugin

can be tested with TestKit and published to the Plugin Portal or a Maven repository

The running example

The examples use the tidepool toolkit of this section: a tidepool-model library, a tidepool-engine library that reads station data with Apache Commons CSV, and a tidepool-cli command line application built on picocli. The sources are the same as in the rest of the section; only the build scripts are shown, because they are what plugins are about. The build starts with the smallest useful set of plugins: the core plugins java-library and application, one community plugin from the Plugin Portal (io.github.ben-manes.versions, 0.66.0 on 2026-10-16, which adds a dependencyUpdates report task) and one settings plugin (the Foojay toolchains resolver, 1.0.0).

Kotlin DSL: settings.gradle.kts
pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}

rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
Groovy DSL: settings.gradle
pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}

rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
Kotlin DSL: build.gradle.kts (root project)
plugins {
    id("io.github.ben-manes.versions") version "0.66.0" apply false
}
Groovy DSL: build.gradle (root project)
plugins {
    id 'io.github.ben-manes.versions' version '0.66.0' apply false
}
Kotlin DSL: tidepool-engine/build.gradle.kts
plugins {
    `java-library`
    id("io.github.ben-manes.versions")
}

repositories {
    mavenCentral()
}

dependencies {
    api(project(":tidepool-model"))
    implementation("org.apache.commons:commons-csv:1.14.1")
}
Groovy DSL: tidepool-engine/build.gradle
plugins {
    id 'java-library'
    id 'io.github.ben-manes.versions'
}

repositories {
    mavenCentral()
}

dependencies {
    api project(':tidepool-model')
    implementation 'org.apache.commons:commons-csv:1.14.1'
}

tidepool-model/build.gradle(.kts) contains only java-library (Kotlin) or id 'java-library' (Groovy) in its plugins { } block, and tidepool-cli/build.gradle(.kts) applies application, depends on project(":tidepool-engine") and info.picocli:picocli:4.7.7, and sets application { mainClass = "io.tidepool.cli.Main" }. Building the whole thing with both DSLs gave (task lines trimmed; their order is not fixed):

...
BUILD SUCCESSFUL in 1s
9 actionable tasks: 9 executed
Configuration cache entry stored.

A second ./gradlew build reported 9 actionable tasks: 9 up-to-date and Configuration cache entry reused., so the build, including the community plugin, is configuration cache compatible. The Plugin Portal page of the plugin carries the same statement as a "CC-compatible" badge (and one for Isolated Projects, see Configuration Cache & Isolated Projects); a plugin that is not compatible makes the whole build incompatible, whatever your own scripts do. Running the application, ./gradlew -q :tidepool-cli:run --args="../data/stations.csv SF", printed San Francisco: -0,869 m in both DSLs (the decimal comma is the default locale of the machine that ran it).

The sections below take this build apart: which plugins are core, what the Portal is, what plugins { } accepts, what pluginManagement { } adds, and the older ways.

Core plugins

A core plugin is a plugin of the distribution. It has a short id (java, application) that is a shorthand for the id in the org.gradle namespace (org.gradle.java), and Gradle never downloads it. Because it is versioned with Gradle, it cannot carry a version of its own, and Gradle says so:

* What went wrong:
Error resolving plugin [id: 'java-library', version: '1.0']
> Plugin 'java-library' is a core Gradle plugin, which cannot be specified with a version number. Such plugins are versioned as part of Gradle. Please remove the version number from the declaration.

The same error text appeared for id("java-library") version "1.0" in the Kotlin DSL and id 'java-library' version '1.0' in the Groovy DSL. The reverse mistake, apply false on a core plugin in the root script (a habit copied from community plugins), is also rejected:

* What went wrong:
Error resolving plugin [id: 'org.gradle.java-library', apply: false]
> Plugin 'org.gradle.java-library' is a core Gradle plugin, which is already on the classpath. Requesting it with the 'apply false' option is a no-op.

The Groovy DSL printed the same message with the id shown as java-library. The second message also explains why core plugins need no repository: they are on the Gradle classpath from the start.

The plugin reference of the user manual lists the core plugins by purpose. The table groups the 42 ids that apply to a project; each id was applied to a project of its own in a scratch build (one plugins { id("...") } per project, ./gradlew --continue help) and every one resolved on Gradle 9.8.1 on macOS. That a plugin applies does not mean it is useful on the machine: the native ones need a C++ or Swift toolchain to build anything.

Purpose Plugin ids

JVM languages and frameworks

java, java-library, java-platform, groovy, scala, antlr, jvm-test-suite, test-report-aggregation, java-test-fixtures, jvm-toolchains

Native languages

cpp-application, cpp-library, cpp-unit-test, swift-application, swift-library, xctest

Packaging and distribution

application, war, ear, maven-publish, ivy-publish, distribution, java-library-distribution

Code analysis

checkstyle, pmd, jacoco, jacoco-report-aggregation, codenarc

IDE integration

eclipse, eclipse-wtp, idea, visual-studio, xcode

Utility

base, wrapper, build-init, signing, java-gradle-plugin, project-report, build-dashboard, version-catalog, groovy-gradle-plugin

Besides these, the reference lists jvm-toolchain-management, which is a settings plugin that configures where JDKs are downloaded from (the Foojay plugin of the example builds on it). Several plugins on the list apply others: java-library and application both apply java, and the lifecycle tasks clean, assemble, check and build come from the lifecycle plugin that base builds on. The next section shows how to see the complete result of applying a few plugins, which is much longer than the plugins named in the script. The documentation of each plugin has its own page: the Java plugins are on Java Projects, testing and reports on Testing JVM Projects, maven-publish and signing on Publishing, java-gradle-plugin on Binary Plugins, and build-init and wrapper on Installation and Gradle Wrapper.

Core plugins also leave the distribution over time. The maven plugin and uploadArchives were removed in Gradle 7.0, and FindBugs, JDepend, the build announcements plugins, the build comparison plugin and the JavaScript plugins were removed in 6.0; the replacements (SpotBugs, maven-publish) are community plugins or newer core plugins. The table at the end of the page lists the plugins that Gradle in Action (written for Gradle 1.7) uses and that no longer exist.

Seeing what a project has applied

There is no built-in task that prints "the plugins of this project", because the answer is a list of class names that is rarely what you want. Three views are available without writing code. The tasks a plugin adds appear in ./gradlew :tidepool-engine:tasks --all, and ./gradlew help --task dependencyUpdates prints a line such as registered by plugin 'io.github.ben-manes.versions'. The classpath that carries the community plugins of a project is shown by buildEnvironment, which the next section uses. To list the applied plugin classes, a few lines at the end of the build script do it. The first version, at the bottom of tidepool-engine/build.gradle.kts, is a configuration cache trap that the next paragraph explains:

val applied = plugins.map { it.javaClass.name.removeSuffix("_Decorated") }.sorted()

tasks.register("appliedPlugins") {
    doLast {
        applied.forEach { println(it) }
    }
}

Plugins applied after the line that computes the list are not in it, so this goes last in the script. With the configuration cache on, the first run printed the list and then failed while storing the entry, and the next run failed the same way:

* What went wrong:
Configuration cache problems found in this build.

1 problem was found storing the configuration cache.
- Task `:tidepool-engine:appliedPlugins` of type `org.gradle.api.DefaultTask`: cannot serialize Gradle script object references as these are not supported with the configuration cache.
  See https://docs.gradle.org/9.8.1/userguide/configuration_cache_requirements.html#config_cache:requirements:disallowed_types

applied is a property of the compiled Kotlin script object, and the task action refers to it, so the task would have to drag the script with it. The fix is to copy the value into a local variable inside the task configuration block, which the action captures instead (the Groovy version has the same shape with def names = applied):

Kotlin DSL: appended to tidepool-engine/build.gradle.kts
val applied = plugins.map { it.javaClass.name.removeSuffix("_Decorated") }.sorted()

tasks.register("appliedPlugins") {
    val names = applied
    doLast {
        names.forEach { println(it) }
    }
}
Groovy DSL: appended to tidepool-engine/build.gradle
def applied = plugins.collect { it.class.name.replace('_Decorated', '') }.sort()

tasks.register('appliedPlugins') {
    def names = applied
    doLast {
        names.each { println it }
    }
}

./gradlew -q :tidepool-engine:appliedPlugins printed, for the Kotlin DSL (the Groovy DSL printed the same list without the KotlinScriptBasePlugin line):

com.github.benmanes.gradle.versions.VersionsPlugin
org.gradle.api.plugins.BasePlugin
org.gradle.api.plugins.HelpTasksPlugin
org.gradle.api.plugins.JavaBasePlugin
org.gradle.api.plugins.JavaLibraryPlugin
org.gradle.api.plugins.JavaPlugin
org.gradle.api.plugins.JvmEcosystemPlugin
org.gradle.api.plugins.JvmTestSuitePlugin
org.gradle.api.plugins.JvmToolchainsPlugin
org.gradle.api.plugins.ReportingBasePlugin
org.gradle.api.plugins.SoftwareReportingTasksPlugin
org.gradle.buildinit.plugins.BuildInitPlugin
org.gradle.buildinit.plugins.WrapperPlugin
org.gradle.kotlin.dsl.provider.plugins.KotlinScriptBasePlugin
org.gradle.language.base.plugins.LifecycleBasePlugin
org.gradle.testing.base.plugins.TestSuiteBasePlugin

Two plugins were named in the script (java-library and the community plugin); fourteen more came with them. Both runs of the corrected task were configuration cache compatible: the second run reported Configuration cache entry reused. Gradle adds the _Decorated suffix to the class names of plugins it instantiates, which the script strips. The rules for what a task action may capture are on Writing Custom Tasks and Lazy Configuration & Providers.

Community plugins and the Gradle Plugin Portal

A community plugin is any non-core plugin that a repository serves. The usual repository is the Gradle Plugin Portal, https://plugins.gradle.org/, a searchable catalogue of plugins with one page per plugin and version. A plugin page has everything needed to use the plugin: the id, the author, the source link, the versions with their release dates, the Gradle features the plugin declares compatibility with (configuration cache, Isolated Projects), and ready-made snippets for the plugins { } block, for a buildSrc/convention-plugin dependency and for the legacy buildscript form. The Portal is also a Maven-layout repository at https://plugins.gradle.org/m2/, which is what the gradlePluginPortal() function of a repository block means, and it can be mirrored by an artifact repository manager (the Portal documentation says to mirror Central as well, because plugins have dependencies there, and to allow plugins-artifacts.gradle.org, where the artifact downloads are redirected). Publishing to the Portal is described on Testing & Publishing Plugins.

The plugin marker artifact

A plugins { } entry holds only an id and a version, which are not Maven coordinates. Gradle bridges the two with a plugin marker artifact whose coordinates are derived from them: group plugin.id, artifact plugin.id.gradle.plugin, version plugin.version. The marker is an empty POM whose only content is a dependency on the real implementation. The one for the plugin of the example, fetched from the Portal:

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0">
      <modelVersion>4.0.0</modelVersion>
      <groupId>io.github.ben-manes.versions</groupId>
      <artifactId>io.github.ben-manes.versions.gradle.plugin</artifactId>
      <version>0.66.0</version>
      <packaging>pom</packaging>
      <dependencies>
        <dependency>
          <groupId>io.github.ben-manes</groupId>
          <artifactId>gradle-versions-plugin</artifactId>
          <version>0.66.0</version>
        </dependency>
      </dependencies>
    </project>

Once resolved, the plugin is an ordinary dependency of the build script, which buildEnvironment shows. For the root project of the example (the output is cut after the first dependency; it continues with Moshi, Okio and Kotlin libraries, one of which Gradle replaced by the Kotlin version it embeds):

classpath
+--- io.github.ben-manes.versions:io.github.ben-manes.versions.gradle.plugin:0.66.0
|    \--- io.github.ben-manes:gradle-versions-plugin:0.66.0
|         \--- com.squareup.moshi:moshi-kotlin:1.15.2
...

buildEnvironment for :tidepool-engine printed No dependencies under classpath: the engine project did not declare the plugin version, it only asked for the plugin that the root project had already put on the shared build script classpath. That sharing is what apply false in the root script is for, and it is also the reason for one of the errors below. Because the implementation is a normal dependency, it is cached in the Gradle User Home like any other (see Directories & Logging), it follows the repositories declared for plugins, and for a repository that does not carry the plugin the build fails with the "not found" error shown below.

Choosing a community plugin

The Portal accepts any plugin that passes its checks, so a plugin on it is available, not endorsed. Before adding one it is worth checking, on its Portal page and repository, that:

  • it is maintained and has a release for the Gradle you run (the example plugin published 0.66.0 on 2026-10-12, one week before the Gradle 9.8.1 run of this page); plugins built for Gradle 8 can break on Gradle 9, which removed APIs that 8.x deprecated;

  • it declares configuration cache support, which the Portal shows as a badge (the plugins you apply are part of the cacheability of the build);

  • its id is the current one. Plugin ids move: asking for the old id of the example plugin, com.github.ben-manes.versions, still worked on 0.66.0 and printed The com.github.ben-manes.versions plugin id is deprecated; apply io.github.ben-manes.versions instead. The sample on the Portal page of a plugin is always the current id;

  • you can account for what it pulls in: it runs with the full power of your build, which is why supply-chain controls apply to plugins just as to libraries (see Supply-Chain Security for JVM Builds).

The user manual’s best practice adds that you should upgrade Gradle before the plugins and test compatibility of plugin upgrades in a separate job, and that you should never assume a plugin is applied before or after another: application order is deterministic but changes when a project is added or a convention plugin applies another plugin.

How a plugin request is resolved

When a plugins { } entry is processed, Gradle looks for the id in an order that the "not found" error spells out: the core plugins, then the plugins defined by included builds (and buildSrc), then the plugin repositories.

flowchart TD req["plugins block entry: id and optional version"] --> core{"core plugin? (short name or org.gradle namespace)"} core -- yes --> usecore["load from the distribution; a version is an error"] core -- no --> inc{"defined by an included build or buildSrc?"} inc -- yes --> local["use the local plugin"] inc -- no --> repo["look for the marker id:id.gradle.plugin:version in the pluginManagement repositories, in order"] repo --> found{"found?"} found -- yes --> add["add the implementation to the build script classpath and apply it"] found -- no --> notfound["error: not found in any of the following sources"]

A request for a plugin that a parent project or the settings has already put on the classpath is a separate case: the same version, or no version, reuses it, and a different version is an error (shown below).

The "not found" error lists the three sources. For an id that does not exist and has no version, the repositories line says why it could not look:

* What went wrong:
Plugin [id: 'tidepool.nope'] was not found in any of the following sources:

- Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
- Included Builds (No included builds contain this plugin)
- Plugin Repositories (plugin dependency must include a version number for this source)

With a version the repositories are searched, and the message shows the marker coordinates that were looked up and the repositories that were consulted (the example build had the Plugin Portal, printed as "Gradle Central Plugin Repository", and Maven Central, printed as "MavenRepo", in its pluginManagement). It is the same text in both DSLs:

* What went wrong:
Plugin [id: 'tidepool.nope', version: '1.0'] was not found in any of the following sources:

- Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
- Included Builds (No included builds contain this plugin)
- Plugin Repositories (could not resolve plugin artifact 'tidepool.nope:tidepool.nope.gradle.plugin:1.0')
  Searched in the following repositories:
    Gradle Central Plugin Repository
    MavenRepo

The plugins block

The plugins { } block is the recommended way to apply a plugin: the user manual’s best practices say to always use it. Because the block can only contain a restricted form, Gradle can read it before running the rest of the script and resolve the plugins early, and the Kotlin DSL can generate type-safe accessors for what they add (see Kotlin DSL and the description of the block on Settings & Build Scripts). The permitted forms are:

Entry Meaning

id("java-library") (Kotlin), id 'java-library' (Groovy)

a core plugin, or a plugin already on the classpath, applied

java-library , application (Kotlin only)

the Kotlin DSL accessor for a core plugin id with a short name; java-library needs backticks because of the hyphen

id("com.example.plugin") version "1.2.3"

a community plugin of that version, resolved and applied; the method form id("...").version("...") is the same

id("com.example.plugin") version "1.2.3" apply false

resolved and put on the classpath, but not applied to this project

id("com.example.plugin")

the same plugin, in a project whose parent or settings has already made it available

alias(libs.plugins.name)

a plugin declared in a version catalog (below)

The syntax is deliberately constrained. The id and the version must be constant string literals, so a variable, a property or a function call is rejected; the block must be a top-level statement of the script, so an if or loop around it is rejected; and it is available only in the build scripts and settings scripts of a build, not in script plugins or init scripts. The constraint is what makes the block "idempotent and side effect free", and the escape for a version that has to come from a property is pluginManagement, below. These restrictions let Gradle optimise the loading and reuse of plugin classes and tell editors what the plugins add to the script. The statement about position differs between the two languages: in the Groovy DSL any other statement before plugins { } is a compile error (the exact message is on the Settings & Build Scripts page), in the Kotlin DSL a group = "..." before the block compiled and ran. Keep plugins { } first in both.

Nesting is rejected differently in each language. In the Kotlin DSL (if (true) { plugins { `java-library` } }):

* What went wrong:
The plugins {} block must not be used here. If you need to apply a plugin imperatively, please use apply<PluginType>() or apply(plugin = "id") instead.

In the Groovy DSL the same nesting is not even recognised as a block, and Groovy looks for an ordinary method called plugins:

* What went wrong:
A problem occurred evaluating project ':tidepool-model'.
> Could not find method plugins() for arguments [build_77rxkrtoz4kfjna62kzgf7ilf$_run_closure1@7c8d72ad] on project ':tidepool-model' of type org.gradle.api.Project.

(The closure name in the message is generated and differs on every run.)

Versions, apply false and the root project

The version of a community plugin can be written in exactly one place per build, and there are two usual places. The one in the running example is the root script: id("io.github.ben-manes.versions") version "0.66.0" apply false resolves the plugin, puts it on the classpath shared by all projects, and does not apply it to the root project. A subproject then writes id("io.github.ben-manes.versions") with no version. Without apply false the plugin would also be applied to the root project, which is what you want for a plugin that is meant for the root. The user manual presents the combination as the preferred way to apply a plugin to some of the subprojects, and the alternative of allprojects { apply(plugin = "...") } or subprojects { } in the root script as "Not Recommended"; the recommended alternative is a convention plugin (Convention Plugins & Sharing Build Logic).

Three errors belong to this arrangement. Asking again for the plugin with a different version in a subproject fails, because the plugin is already on the classpath at the version the root requested:

* What went wrong:
Error resolving plugin [id: 'io.github.ben-manes.versions', version: '0.65.0']
> The request for this plugin could not be satisfied because the plugin is already on the classpath with a different version (0.66.0).

Asking for the same version in a subproject worked (the build ran). Asking for a plugin without a version that nothing has made available fails with the "not found" message above. And a settings plugin written in a project’s plugins { } block fails at application time, because it targets the wrong object:

* What went wrong:
An exception occurred applying plugin request [id: 'org.gradle.toolchains.foojay-resolver-convention', version: '1.0.0']
> Failed to apply plugin 'org.gradle.toolchains.foojay-resolver-convention'.
   > Settings plugins must be applied in the settings script.

Version catalog aliases

When the build has a version catalog, a plugin can be declared there once and applied with alias. The file below, gradle/libs.versions.toml, is a plain text file (the TOML syntax is described on Version Catalogs):

[versions]
versions-plugin = "0.66.0"

[plugins]
versions = { id = "io.github.ben-manes.versions", version.ref = "versions-plugin" }
Kotlin DSL: build.gradle.kts (root) and tidepool-engine/build.gradle.kts
// build.gradle.kts
plugins {
    alias(libs.plugins.versions) apply false
}

// tidepool-engine/build.gradle.kts
plugins {
    `java-library`
    alias(libs.plugins.versions)
}
Groovy DSL: build.gradle (root) and tidepool-engine/build.gradle
// build.gradle
plugins {
    alias(libs.plugins.versions) apply false
}

// tidepool-engine/build.gradle
plugins {
    id 'java-library'
    alias(libs.plugins.versions)
}

Both builds succeeded with the configuration cache on, and :tidepool-engine:tasks --all listed dependencyUpdates. The catalog moves both the plugin id and its version out of the scripts, and a plugin added to the catalog gets an accessor that the IDE completes.

pluginManagement

pluginManagement { } in the settings file is the configuration point for everything about finding plugins: where the repositories are, which versions plugins get, and how an id maps to an artifact. It must be the first block of the settings script (the earlier page shows the error for the wrong order). Without it, plugins resolve from the Gradle Plugin Portal only; the example lists the Portal and Maven Central because some plugins and some of their dependencies are published only to Central. The block also exists for init scripts, inside settingsEvaluated { } (not run here).

Plugin repositories

The repositories { } block of pluginManagement is searched in order, and a plugin is looked for in each repository until one has it. These repositories are entirely separate from the repositories { } that supply a project’s libraries: the former serve plugins and the build script classpath, the latter serve compile and runtime dependencies. A private Maven or Ivy repository is declared with maven { url = uri("...") } or ivy { ... }, and the documented form that puts a local repository ahead of the Portal is:

pluginManagement {
    repositories {
        maven(url = file("./maven-repo"))
        gradlePluginPortal()
        ivy(url = file("./ivy-repo"))
    }
}

(That block is the one printed in the user manual and was not run here; the real runs of this page used the Portal and Central.) A repository that serves plugins must contain the marker artifacts as well as the implementations: a repository that merely mirrors a library does not satisfy plugins { id("...") version "..." }. The buildscript { repositories { } } block of the legacy form is a separate list for that script.

Plugins and the Gradle Plugin Portal do not always agree about Maven Central. The implementation of the example plugin was not on Maven Central on 2026-10-16: when a resolution rule (below) pointed at its implementation coordinates and the only plugin repository was Maven Central, the build failed:

* What went wrong:
Plugin [id: 'io.github.ben-manes.versions', version: '0.66.0', artifact: 'io.github.ben-manes:gradle-versions-plugin:0.66.0', apply: false] was not found in any of the following sources:

- Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
- Included Builds (No included builds contain this plugin)
- Plugin Repositories (could not resolve plugin artifact 'io.github.ben-manes:gradle-versions-plugin:0.66.0')

Plugin versions in one place

pluginManagement { plugins { } } gives an id a version for the whole build, so that the build scripts, wherever they are, write the id alone. It does not have the constrained syntax of the build script block: the version can be computed, in particular read from gradle.properties, which makes it possible to change a version with a -P option or in a single line. The example moves the version of the community plugin there, from a property named versionsPluginVersion:

org.gradle.configuration-cache=true
versionsPluginVersion=0.66.0
Kotlin DSL: settings.gradle.kts
pluginManagement {
    val versionsPluginVersion = providers.gradleProperty("versionsPluginVersion").get()

    repositories {
        gradlePluginPortal()
        mavenCentral()
    }

    plugins {
        id("io.github.ben-manes.versions") version versionsPluginVersion
    }
}

plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}

rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
Groovy DSL: settings.gradle
pluginManagement {
    def versionsPluginVersion = providers.gradleProperty('versionsPluginVersion').get()

    repositories {
        gradlePluginPortal()
        mavenCentral()
    }

    plugins {
        id 'io.github.ben-manes.versions' version versionsPluginVersion
    }
}

plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}

rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'

The root script then declares id("io.github.ben-manes.versions") apply false (Groovy: id 'io.github.ben-manes.versions' apply false) with no version, and the engine’s script is unchanged. Both builds ran with the configuration cache on, and :tidepool-engine:dependencyUpdates produced its report in a second run that reused the cache entry. The version of the foojay settings plugin stays in the settings plugins { } block: the plugin versions of pluginManagement apply to project build scripts. The one-line form people copied for years, val versionsPluginVersion: String by settings, is deprecated by Gradle 9.6 (it printed the by settings delegate deprecation under --warning-mode all in a run of this page), which is why the example uses providers.gradleProperty(...); see Build Environment & Properties.

Plugin resolution rules

resolutionStrategy { eachPlugin { } } can change a plugin request before it is resolved: the requested id, version and artifact are visible as requested, and useModule and useVersion override them. The documented use is a plugin that was published without plugin markers, so that the id has to be mapped to an artifact by hand. The example makes the same plugin resolve by implementation coordinates instead of its marker (the version being taken from the request, so that only requests that carry a version are rewritten):

Kotlin DSL: settings.gradle.kts
pluginManagement {
    repositories {
        gradlePluginPortal()
    }

    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "io.github.ben-manes.versions" && requested.version != null) {
                useModule("io.github.ben-manes:gradle-versions-plugin:${requested.version}")
            }
        }
    }
}

rootProject.name = "tidepool"
include("tidepool-model", "tidepool-engine", "tidepool-cli")
Groovy DSL: settings.gradle
pluginManagement {
    repositories {
        gradlePluginPortal()
    }

    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == 'io.github.ben-manes.versions' && requested.version != null) {
                useModule("io.github.ben-manes:gradle-versions-plugin:${requested.version}")
            }
        }
    }
}

rootProject.name = 'tidepool'
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'

With --info the resolution of the root request printed the rewritten artifact, and the subproject request without a version was left alone:

Resolved plugin [id: 'io.github.ben-manes.versions', version: '0.66.0', artifact: 'io.github.ben-manes:gradle-versions-plugin:0.66.0', apply: false]
Resolved plugin [id: 'org.gradle.application']
Resolved plugin [id: 'org.gradle.java-library']
Resolved plugin [id: 'io.github.ben-manes.versions']
Resolved plugin [id: 'org.gradle.java-library']

The --info log is the quickest way to see what Gradle did with each request. Resolution rules are an escape hatch; when a plugin has markers, as nearly every Portal plugin does, none is needed. The third thing pluginManagement accepts is includeBuild("build-logic"), which makes the plugins of an included build, normally the organisation’s convention plugins, resolvable by id; it is the subject of Convention Plugins & Sharing Build Logic (not run here) and of Composite Builds.

The legacy way: apply and buildscript

Before the plugins { } block (introduced in Gradle 2.1, together with the Plugin Portal), a build listed the plugin as a dependency of the build script and then applied it. The form still works and is documented as legacy. Here is the engine of the example written that way:

Kotlin DSL: build.gradle.kts (root) and tidepool-engine/build.gradle.kts
// build.gradle.kts
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("io.github.ben-manes:gradle-versions-plugin:0.66.0")
    }
}

// tidepool-engine/build.gradle.kts
apply(plugin = "java-library")
apply(plugin = "io.github.ben-manes.versions")

repositories {
    mavenCentral()
}

dependencies {
    add("api", project(":tidepool-model"))
    add("implementation", "org.apache.commons:commons-csv:1.14.1")
}
Groovy DSL: build.gradle (root) and tidepool-engine/build.gradle
// build.gradle
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath 'io.github.ben-manes:gradle-versions-plugin:0.66.0'
    }
}

// tidepool-engine/build.gradle
apply plugin: 'java-library'
apply plugin: 'io.github.ben-manes.versions'

repositories {
    mavenCentral()
}

dependencies {
    api project(':tidepool-model')
    implementation 'org.apache.commons:commons-csv:1.14.1'
}

Both versions built :tidepool-engine and listed dependencyUpdates. The differences from the plugins { } block are the point of avoiding it:

  • the coordinates are the implementation’s Maven coordinates, not the id, and you have to know them (the marker artifact exists so that you do not);

  • the repository is declared in the script itself, so pluginManagement repositories do not apply;

  • the plugin and its dependencies are added to the classpath of the script as ordinary dependencies, so nothing checks that two scripts ask for the same version of the plugin, which the plugin DSL does (the "already on the classpath" error above);

  • the Kotlin DSL cannot generate type-safe accessors for a plugin applied this way, so api(...) and implementation(...) are not available and the example has to use add("api", ...). The effect on java { } and on dependencies { } is demonstrated on Settings & Build Scripts;

  • the Groovy DSL is unaffected because it resolves everything dynamically.

The legacy form is still the right tool in two situations: a library needed by the build script itself (a YAML or JSON parser used in the script body), which belongs in buildscript { dependencies { classpath(...) } }, and a script plugin applied with apply(from = "other.gradle.kts"), where a plugins { } block is not allowed. Note that apply(plugin = "...") also takes a class, apply<JavaPlugin>() in the Kotlin DSL and apply plugin: JavaPlugin in Groovy.

Project, settings and init plugins

A plugin is a class with one method, apply(target), and the type of target decides where it can be applied. Gradle has three scopes:

Interface Scope Applied in Used to

Plugin<Project>

one project

build.gradle(.kts), plugins { } or apply

register tasks, apply other plugins, configure the project

Plugin<Settings>

the whole build

settings.gradle(.kts), plugins { }

include projects, configure plugin management, dependency resolution, the build cache, toolchain download repositories

Plugin<Gradle>

the build invocation (and, from init.d, every build of the user)

an init script

inject repositories or enterprise settings into all builds on a machine

For a precompiled script plugin the scope comes from the file name: name.gradle.kts is a project plugin, name.settings.gradle.kts is a settings plugin and name.init.gradle.kts is an init plugin (and the same with .gradle for the Groovy DSL). A file called name.gradle.kts is a project plugin even if a settings script applies it. The settings plugin of the running example, the Foojay resolver, was shown in the settings plugins { } block and its failure when applied to a project above. See Settings & Build Scripts for what a settings script can do.

An init script can apply a plugin to every project of every build it is used with, which is how an organisation enforces a convention or a report without touching the repositories. The example applies the core project-report plugin, whose projectReport task writes reports about the project, to all projects, from a file that is not part of the project:

Kotlin DSL: reports.init.gradle.kts
allprojects {
    apply(plugin = "project-report")
}
Groovy DSL: reports.init.gradle
allprojects {
    apply plugin: 'project-report'
}

Passed with -I, in both DSLs and with the configuration cache on, ./gradlew -q -I reports.init.gradle.kts :tidepool-model:tasks --group=reporting printed:

------------------------------------------------------------
Tasks runnable from project ':tidepool-model'
------------------------------------------------------------

Reporting tasks
---------------
projectReport - Generates a report about your project.

(the Groovy script printed the same). Without -I the same command printed No tasks for the group. The -I option is a one-off; a script in init.d of the Gradle User Home applies to every build of the user. The location and the init script lifecycle are on Init Scripts & the Tooling API and Directories & Logging.

Local plugins

A plugin does not have to be published. The directory buildSrc, next to settings.gradle(.kts), is built before the rest of the build and its output goes on the classpath of every build script; a precompiled script plugin placed there is available by id. The example adds a tiny plugin, tidepool.stamp, which registers a stamp task in the tidepool group. It is a script in the build DSL; the file name is the plugin id.

Kotlin DSL: buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
}
Kotlin DSL: buildSrc/src/main/kotlin/tidepool.stamp.gradle.kts
plugins {
    base
}

tasks.register("stamp") {
    group = "tidepool"
    description = "Prints the name of the project that applied this plugin."
    val projectName = project.name
    doLast {
        println("stamped by $projectName")
    }
}
Groovy DSL: buildSrc/build.gradle
plugins {
    id 'groovy-gradle-plugin'
}
Groovy DSL: buildSrc/src/main/groovy/tidepool.stamp.gradle
plugins {
    id 'base'
}

tasks.register('stamp') {
    group = 'tidepool'
    description = 'Prints the name of the project that applied this plugin.'
    def projectName = project.name
    doLast {
        println "stamped by ${projectName}"
    }
}

tidepool-model/build.gradle(.kts) then applies it by id:

plugins {
    `java-library`
    id("tidepool.stamp")
}

./gradlew -q :tidepool-model:stamp printed stamped by tidepool-model in both DSLs, and :tidepool-model:tasks --group=tidepool listed stamp under "Tidepool tasks". The configuration cache was on and the second run reused the entry, so the plugin is compatible: it copies project.name into a local value at configuration time and not inside doLast, which is the pattern of the previous sections. buildSrc is a separate build with its own repositories. The Kotlin variant needs one even though the kotlin-dsl plugin id resolves without it, because the plugin compiles the scripts against Kotlin libraries: without the repositories { gradlePluginPortal() } block of the example, the run failed with

* What went wrong:
Execution failed for task ':buildSrc:generateExternalPluginSpecBuilders' (registered by plugin class 'org.gradle.kotlin.dsl.plugins.precompiled.PrecompiledScriptPlugins').
> Error while evaluating property 'pluginEntries' of task ':buildSrc:generateExternalPluginSpecBuilders'.
   > Could not resolve all files for configuration ':buildSrc:compileClasspath'.
      > Cannot resolve external dependency org.jetbrains.kotlin:kotlin-stdlib:2.4.10 because no repositories are defined.
        Required by:
            project ':buildSrc'

(the output continued with the same message for kotlin-reflect). The Groovy variant, groovy-gradle-plugin, ran without a repository block. Two limitations of the plugin id are documented: it cannot start with org.gradle and it cannot equal a core plugin id.

buildSrc is the simplest place for build logic and has a cost: any change in it makes the entire build out of date. An included build, for example build-logic, added with pluginManagement { includeBuild("build-logic") }, holds the same precompiled script plugins with fewer invalidations, and the user manual’s best practices name it as the preferred location of build logic, in particular of convention plugins. Standalone binary plugins, written as classes, are on Binary Plugins. Both are covered on Convention Plugins & Sharing Build Logic.

The plugin reference and where to read on

The user manual has a reference page that lists each core plugin with a one-line description and a link, grouped as in the table above; the Gradle DSL reference documents the extension and task types a plugin adds, and the Javadoc documents the plugin classes. The sibling pages of this section that describe particular plugins are listed in the previous sections. For a community plugin the documentation is the Portal page and the plugin’s own repository, and the plugins { } snippet on its Portal page is the authoritative id and version to copy.

Gradle 8.14 and earlier releases

The plugin DSL is old and stable, so most of this page applies unchanged to Gradle 8.14, the last 8.x minor (not run here: 8.14 was not available). From the release notes of the versions that introduced each feature: the plugins { } block and the Plugin Portal arrived in Gradle 2.1; the pluginManagement { } block and plugin resolution rules in 3.5 (replacing pluginRepositories in the settings file); and pluginManagement { plugins { } }, with versions read from gradle.properties, in 5.6. Plugin version catalog aliases came with version catalogs (stable in 7.4). What Gradle 9.0 changed for plugins is mostly about the plugins themselves: it removed APIs that 8.x had deprecated (among them conventions, Project.exec/javaexec and the "name"() task syntax), runs on JVM 17 or later, embeds Kotlin 2.2 and Groovy 4, and requires Kotlin Gradle plugin 2.0.0 or newer. A community plugin that has not been updated since 8.x can therefore fail on 9.x with messages that point at the plugin’s code, and the remedy is to upgrade the plugin (the second reason to prefer plugins that declare their compatibility), or to replace it. The by settings, by project and by extra property delegates that many pluginManagement examples used are deprecated from 9.6 (see above). The matrix of what runs where is on Versions, Compatibility & Release History and the upgrade path on Upgrading Gradle.

Where Gradle in Action is outdated

Gradle in Action (Muschko, Manning, 2014) was written for Gradle 1.7, before the Plugin Portal existed, and its plugin chapters reflect that. Current status of the statements about plugins, from the official documentation:

Book statement or practice Current status Source

plugins are applied with apply plugin: 'x' and there is no Plugin Portal

the plugins { } block (Gradle 2.1) and the Plugin Portal are the standard way

User Manual: Using Plugins

the maven plugin and uploadArchives publish artifacts

removed in Gradle 7.0; use maven-publish

Upgrading from 6.x

the jetty plugin (jettyRun) runs a web application

deprecated in 3.0 and removed in 4.0 (Gretty is a third-party alternative)

Gradle 4.0 Release Notes

FindBugs, JDepend, and the build announcements plugins

deprecated in 5.0 and removed in 6.0 (SpotBugs is third party)

Upgrading from 5.x

the build comparison plugin and the JavaScript plugins (rhino, envjs, coffeescript, jshint)

removed in 6.0

Upgrading from 5.x

the sonar and sonar-runner plugins

the legacy Sonar plugin was removed in 3.0; use the SonarQube plugin org.sonarqube

Gradle 3.0 Release Notes

a Cobertura plugin for coverage

the Cobertura plugin was always third party; JaCoCo is the core plugin

User Manual: The JaCoCo Plugin

allprojects { } and subprojects { } to inject configuration into projects

convention plugins in buildSrc or an included build

Convention plugins vs cross-configuration

the Groovy DSL only

Kotlin DSL 1.0 in 5.0, and the default for gradle init since 8.2

Gradle 8.2 Release Notes

JCenter and Bintray as the repository for plugins and libraries

jcenter() was removed in 9.0 and Bintray was shut down in 2021

Upgrading to Gradle 9

plugin tests with ProjectBuilder only

TestKit GradleRunner for functional tests

User Manual: Testing with TestKit

Where to go next