Gradle’s Problems API lets plugins report rich, structured information about issues that occur during a build, both warnings and failures.

The following example shows how a plugin can report a problem:

ProblemReportingPlugin.java
public class ProblemReportingPlugin implements Plugin<Project> {

    private final ProblemReporter reporter;
    private final ProblemId pluginMisconfigured;

    interface SomeData extends AdditionalData {
        void setName(String name);
        String getName();
    }

    @Inject
    public ProblemReportingPlugin(Problems problems) { (1)
        reporter = problems.getReporter();
        pluginMisconfigured = problems.getGroups().getGradle().getBuildDefinition().problemId("Plugin 'x' is misconfigured"); (2)
    }

    public void apply(Project project) {
        reporter.report(pluginMisconfigured, builder -> builder (3)
            .details("The property 'outputDirectory' of plugin 'x' is not set")
            .solution("Set 'outputDirectory' in the 'x' extension")
            .additionalData(SomeData.class, additionalData -> {
                additionalData.setName("Some name"); (4)
            })
        );
    }
}
1 Inject the Problems service into the plugin.
2 Create the ProblemId once, in one of the predefined problem groups, and keep it together with the reporter; both are stable for the lifetime of the plugin.
3 Report a recoverable problem so the build can continue.
4 Add optional extra data to the problem.

Where reported problems appear

Gradle delivers reported problems to several places:

  • The HTML Problems Report lists all reported problems. On the command line, this is where warnings appear by default; the console shows only the location of the report at the end of the build.

  • The console always shows failures. Warnings are printed as well when running with --warning-mode=all, see Console rendering. With --warning-mode=none, the console does not show the location of the report either.

  • Tooling API clients, such as IDEs, receive problems as events.

Report a problem once, through the Problems API only. Do not log it again with the task’s logger: with --warning-mode=all and in IDEs, the user would see it twice.

The example below shows the console output for each case. Repeated instances of the same problem are summarized, see Problem Summarization.

Problems API Usage

The following simple plugin adds a greet task, applied by the consumer build of the sample:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
package org.example

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.problems.*
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import javax.inject.Inject

/**
 * Simple demo plugin for the Gradle Problems API.
 */
abstract class HelloProblemsPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.tasks.register("greet", GreetTask::class.java) {
            group = "demo"
            description = "Greets a recipient. Demonstrates Problems API."
        }
    }
}

/**
 * Extra structured data to attach to problem reports.
 * Tools like Build Scans or IDEs could show this.
 */
interface GreetProblemData : AdditionalData {
    var configuredRecipient: String?
}

/**
 * Custom task that uses the Gradle Problems API.
 */
abstract class GreetTask @Inject constructor(private val problems: Problems) : DefaultTask() {

    @get:Input
    @get:Optional
    abstract val recipient: Property<String>

    private val problemGroup = problems.groups.others.group("Hello Problems")
    private val warnId = problemGroup.problemId("Recipient not set")
    private val failId = problemGroup.problemId("Forbidden recipient 'fail'")

    @TaskAction
    fun run() {
        val reporter = problems.reporter
        val name = recipient.orNull?.trim().orEmpty()

        // Warning: missing recipient -> provide a helpful suggestion
        if (name.isEmpty()) {
            reporter.report(warnId) {
                details("No recipient configured")
                solution("""Set the recipient: tasks.greet { recipient = "World" }""")
                documentedAt("https://gradle.org/hello-problems#recipient")
                additionalData(GreetProblemData::class.java) {
                    configuredRecipient = null
                }
            }
        }

        // Fatal: a specific value is disallowed to show throwing()
        else if (name.equals("fail", ignoreCase = true)) {
            throw reporter.throwing(GradleException("forbidden value"), failId) {
                details("Recipient 'fail' is not allowed")
                solution("""Choose another value, e.g. recipient = "World".""")
                documentedAt("https://gradle.org/hello-problems#forbidden")
                additionalData(GreetProblemData::class.java) {
                    configuredRecipient = name
                }
            }
        }

        logger.lifecycle("Hello, $name!")
    }
}
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
package org.example

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.problems.*
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import javax.inject.Inject

/**
 * Simple demo plugin for the Gradle Problems API.
 */
class HelloProblemsPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.tasks.register("greet", GreetTask) { task ->
            task.group = "demo"
            task.description = "Greets a recipient. Demonstrates Problems API."
        }
    }
}

/**
 * Extra structured data to attach to problem reports.
 * Tools like Build Scans or IDEs could show this.
 */
interface GreetProblemData extends AdditionalData {
    String getConfiguredRecipient()
    void setConfiguredRecipient(String value)
}

/**
 * Custom task that uses the Gradle Problems API.
 */
abstract class GreetTask extends DefaultTask {

    @Input
    @Optional
    abstract Property<String> getRecipient()

    private final Problems problems
    private final ProblemId warnId
    private final ProblemId failId

    @Inject
    GreetTask(Problems problems) {
        this.problems = problems
        def problemGroup = problems.groups.others.group("Hello Problems")
        warnId = problemGroup.problemId("Recipient not set")
        failId = problemGroup.problemId("Forbidden recipient 'fail'")
    }

    @TaskAction
    void run() {
        def reporter = problems.reporter
        def name = recipient.orNull?.trim() ?: ""

        // Warning: missing recipient -> provide a helpful suggestion
        if (name.isEmpty()) {
            reporter.report(warnId) { spec ->
                spec.details("No recipient configured")
                    .solution('Set the recipient: tasks.greet { recipient = "World" }')
                    .documentedAt("https://gradle.org/hello-problems#recipient")
                    .additionalData(GreetProblemData) {
                        it.configuredRecipient = null
                    }
            }
        }
        // Fatal: a specific value is disallowed to show throwing()
        else if (name.equalsIgnoreCase("fail")) {
            throw reporter.throwing(new GradleException("forbidden value"), failId) { spec ->
                spec.details("Recipient 'fail' is not allowed")
                    .solution('Choose another value, e.g. recipient = "World".')
                    .documentedAt("https://gradle.org/hello-problems#forbidden")
                    .additionalData(GreetProblemData) {
                        it.configuredRecipient = name
                    }
            }
        }

        logger.lifecycle("Hello, $name!")
    }
}

How it works:

The greet task prints a greeting when used correctly:

$ ./gradlew :consumer:greet -Precipient=World
> Task :consumer:greet
Hello, World!

BUILD SUCCESSFUL in 0s

It reports a warning if no recipient is set. By default, the console only shows the location of the HTML Problems Report:

$ ./gradlew :consumer:greet
> Task :consumer:greet
Hello, !

[Incubating] Problems report is available at: file:///home/user/gradle/samples/build/reports/problems/problems-report.html

BUILD SUCCESSFUL in 0s

With --warning-mode=all, the warning is printed to the console as well:

$ ./gradlew :consumer:greet --warning-mode=all
> Task :consumer:greet
Problem found: Recipient not set (in Others > Hello Problems)
  No recipient configured
    For more information, please refer to https://gradle.org/hello-problems#recipient.
    Possible solution: Set the recipient: tasks.greet { recipient = "World" }.
Hello, !

[Incubating] Problems report is available at: file:///home/user/gradle/samples/build/reports/problems/problems-report.html

BUILD SUCCESSFUL in 0s

It fails if the recipient is fail:

$ ./gradlew :consumer:greet -Precipient=fail
FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':consumer:greet' (registered by plugin 'org.example.hello-problems').
> Forbidden recipient 'fail'
    forbidden value
      Recipient 'fail' is not allowed
      For more information, please refer to https://gradle.org/hello-problems#forbidden.
      Possible solution: Choose another value, e.g. recipient = "World".

* Try:
> Run with --scan to get full insights from a Build Scan (powered by Develocity).

BUILD FAILED in 0s

Injecting the Problems service

Gradle will automatically provide the Problems service when injected into the custom task type, here through its constructor:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
abstract class GreetTask @Inject constructor(private val problems: Problems) : DefaultTask() {
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
private final Problems problems
private final ProblemId warnId
private final ProblemId failId

@Inject
GreetTask(Problems problems) {
    this.problems = problems
    def problemGroup = problems.groups.others.group("Hello Problems")
    warnId = problemGroup.problemId("Recipient not set")
    failId = problemGroup.problemId("Forbidden recipient 'fail'")
}

Choosing a ProblemGroup and creating a ProblemId

Every problem belongs to a ProblemGroup, and is identified by a ProblemId within that group. Groups and IDs let Gradle deduplicate and summarize problems, and let consumers such as IDEs navigate and filter them.

Gradle provides a predefined hierarchy of groups, available from the Problems service as problems.groups. Pick the group matching what the problem is about, or create a subgroup for your plugin below the closest predefined group, then create the problem ID from the group. Groups and IDs are stable values: create them once, for example in the constructor of your plugin, and reuse them at every report site.

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
private val problemGroup = problems.groups.others.group("Hello Problems")
private val warnId = problemGroup.problemId("Recipient not set")
private val failId = problemGroup.problemId("Forbidden recipient 'fail'")
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
def problemGroup = problems.groups.others.group("Hello Problems")
warnId = problemGroup.problemId("Recipient not set")
failId = problemGroup.problemId("Forbidden recipient 'fail'")

Choosing a reporting mode for the ProblemReporter

Plugins can report problems in two ways:

  • report() — use for recoverable problems when the build should continue.

  • throwing() — use for non-recoverable problems that should fail the build.

Once you have decided what type of problem you want to report, get the ProblemReporter using problem.getReporter():

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
val reporter = problems.reporter
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
def reporter = problems.reporter

Reporting a recoverable problem

Use problems.getReporter().report {} to signal something went wrong but allow the build to continue:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
reporter.report(warnId) {
    details("No recipient configured")
    solution("""Set the recipient: tasks.greet { recipient = "World" }""")
    documentedAt("https://gradle.org/hello-problems#recipient")
    additionalData(GreetProblemData::class.java) {
        configuredRecipient = null
    }
}
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
reporter.report(warnId) { spec ->
    spec.details("No recipient configured")
        .solution('Set the recipient: tasks.greet { recipient = "World" }')
        .documentedAt("https://gradle.org/hello-problems#recipient")
        .additionalData(GreetProblemData) {
            it.configuredRecipient = null
        }
}

Reporting a fatal problem

Use problems.getReporter().throwing {} to fail the build with a structured error:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
throw reporter.throwing(GradleException("forbidden value"), failId) {
    details("Recipient 'fail' is not allowed")
    solution("""Choose another value, e.g. recipient = "World".""")
    documentedAt("https://gradle.org/hello-problems#forbidden")
    additionalData(GreetProblemData::class.java) {
        configuredRecipient = name
    }
}
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
throw reporter.throwing(new GradleException("forbidden value"), failId) { spec ->
    spec.details("Recipient 'fail' is not allowed")
        .solution('Choose another value, e.g. recipient = "World".')
        .documentedAt("https://gradle.org/hello-problems#forbidden")
        .additionalData(GreetProblemData) {
            it.configuredRecipient = name
        }
}

This throws an exception tied to a rich problem report so Gradle clients can display meaningful failure info.

Add data using ProblemSpec

When reporting a problem, you can provide a wide variety of metadata such as descriptions, solutions, and additional data. The severity follows from the reporting mode: report() reports a warning, throwing() an error.

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
details("No recipient configured")
solution("""Set the recipient: tasks.greet { recipient = "World" }""")
documentedAt("https://gradle.org/hello-problems#recipient")
additionalData(GreetProblemData::class.java) {
    configuredRecipient = null
}
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
spec.details("No recipient configured")
    .solution('Set the recipient: tasks.greet { recipient = "World" }')
    .documentedAt("https://gradle.org/hello-problems#recipient")
    .additionalData(GreetProblemData) {
        it.configuredRecipient = null
    }

See ProblemSpec for all available fields.

Predefined Problem Groups

Gradle provides a predefined hierarchy of problem groups through ProblemGroups, available from the Problems service as problems.groups. Groups describe what a problem is about, not where or when it happened; the location of a problem is recorded separately. A shared set of groups keeps problem reports consistent across plugins, so that build engineers, IDEs, and other tools can navigate, filter, and aggregate problems reliably.

The hierarchy

The hierarchy has three levels, with these kinds of groups:

  • Root groups only structure the hierarchy. Problems cannot be reported directly into a root group.

  • Subgroups, either predefined or created by a plugin, hold problems and can have one further level of subgroups.

  • Third-level groups, created by a plugin below a subgroup, hold problems and cannot be extended.

  • Gradle’s own subgroups hold problems; plugins cannot create subgroups below them.

  • Undefined groups hold problems and can never be extended.

Every root group and every extensible subgroup also has a synthetic Undefined subgroup, for problems that do not fit any of its other subgroups.

The names shown below identify a group together with its parent. group(name) matches on the name, and console output renders groups as a chain such as Gradle > Build Cache.

Root group Predefined subgroups What it covers

Gradle

Build Cache, Build Definition, Build Logic, Configuration Cache, Deprecation (reserved for Gradle), DSL Evaluation, Invocation, Isolated Projects, Plugin Validation

Initialization, configuration, and execution of the Gradle runtime, integration of built-in and third-party plugins with Gradle. This group is closed: plugins can report into its subgroups but cannot add subgroups. The Deprecation subgroup is reserved for Gradle’s own deprecations; plugins cannot report into it. Consumers of reports and Tooling API events identify it, like any group, by its name path, Gradle then Deprecation.

Dependencies

Declaration, Locking, Graph Resolution, Artifact Resolution, Verification

Declaration, resolution, locking, and verification of dependencies required by projects or the Gradle runtime.

Transformation

Binary Transformation, Code Transformation, Resource Transformation

Generation or manipulation of code, binaries, or resources before running or packaging.

Compilation

C++, Groovy, Java, Kotlin, Scala, Swift

Code compilation, including the compiler’s configuration, compiler invocation, or compiler plugins.

External Processes

Application, Service, Tool

External process invocations of compiled applications, native tools, and other runnable artifacts.

Packaging

Distributions, JAR, Native Linking, Signing

Assembling products into various runnable and deployable forms, including creating binaries, archives, installers, or container images, and signing them.

Verification

Code Coverage, Code Quality, Security, Testing

Verification of software correctness, quality, and compliance through testing, static analysis, code coverage, linting, and other verification mechanisms that ensure the software meets specified requirements and standards, including setup and configuration of required tools and frameworks.

Documentation

Javadoc, Groovydoc, Scaladoc

Production of documentation of any form, including API docs, manuals, or websites.

Provisioning

Infrastructure, Tools and Toolchains

Making resources available for use and managing them, including tools and toolchains, services, or infrastructure.

Delivery

Publishing, Deployment

Making products available to users, including publishing software components, deploying applications, or uploading documentation.

Others

none

Problems that do not fit in any other group.

Each predefined group carries a description, documented in the Javadoc of ProblemGroups and its group types.

Choosing a group

To decide where a problem belongs, consider what the code reporting it is doing and what the problem communicates, then:

  1. Find the root group matching that activity. For example, a problem found while compiling Java code belongs under compilation.

  2. If a predefined subgroup matches, report the problem there: problems.groups.compilation.java.problemId("Unused import").

  3. If no predefined subgroup matches, create a subgroup for your plugin below the root group, or use the root group’s undefined subgroup: problems.groups.transformation.group("KMP").problemId("Bundle failed").

  4. If no root group matches, create a subgroup below others, or use problems.groups.others.undefined.

When several groups match, pick the one closest to the context of your plugin. For example, a task that processes files for documentation reports under documentation rather than transformation, and a problem about a task’s input annotations reports under gradle.pluginValidation even though it surfaces at execution time.

Creating subgroups

Subgroups are created from an existing group with group(name), and problem IDs from a group with problemId(name). The types of the hierarchy enforce the placement rules: a root group only offers group(name), a subgroup offers both, and the closed kinds (third-level groups, Gradle’s own subgroups, and Undefined groups) only offer problemId(name).

Names are case-sensitive. Calling group(name) with the exact name of a predefined subgroup returns that predefined group; any other name creates a new group, even if it differs from a predefined one only by case: problems.groups.compilation.group("Java") is the predefined Java subgroup, problems.groups.compilation.group("java") is a separate group. The name Undefined is reserved in any letter case; use the undefined property instead. Group names are limited to 50 characters and consist of letters and digits of any script, single spaces between words, and the characters -, _, ., `, `/`, `&`, `'` and `#`. They start with a letter or a digit, and must be in Unicode normalization form NFC, so that two names that look the same are the same name. Symbols, quotes and other punctuation are rejected; examples of valid names are `C+, CI/CD, Tools & Toolchains, Русский or 中文.

Problem names are sentences and are less restricted: they must not be blank, must not have leading or trailing whitespace, must not contain control or format characters (other than the zero width joiner and non-joiner), and are limited to 2000 characters.

Keep group names short and noun-based, so they read well as categories in a report. Formulate problem names as a single, concise sentence describing the problem, without embedding contextual details such as file names or values; those belong in the problem’s label, details, and locations.

Problem Summarization

Gradle avoids spamming users with duplicate problem messages:

Build Failures

The standard way to fail a build is by throwing an exception. The Problems API enhances this by allowing you to throw exceptions tied to detailed problem reports:

FailingTask.java
ProblemId id = ProblemId.create("sample-error", "FailingTask execution problem", StandardPlugin.PROBLEM_GROUP);
throw getProblems().getReporter().throwing((new RuntimeException("Message from runtime exception")), id, problemSpec -> {
    problemSpec.contextualLabel("This happened because ProblemReporter.throwing() was called")
        .details("This is a demonstration of how to add\ndetailed information to a build failure")
        .documentedAt("https://example.com/docs")
        .solution("Remove the Problems.throwing() method call from the task action");
});

This ensures build failures are clearly associated with their underlying issues and are visible across all Gradle clients (CLI, Build Scan, IDEs, Tooling API).

Command-line Interface

Gradle CLI output shows detailed information from the reported problem:

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':myFailingTask'.
> FailingTask execution problem  (1)
    This happened because ProblemReporter.throwing() was called  (2)
      This is a demonstration of how to add  (3)
      detailed information to a build failure

* Try:  (4)
> Remove the Problems.throwing() method call from the task action
> Run with --scan to generate a Build Scan (powered by Develocity).
BUILD FAILED in 0ms  (5)
1 The display name of the problem ID.
2 The contextual label (or the supplied exception’s message if the contextual label is missing).
3 The content of the details property.
4 The solutions or recommended actions.
5 The AdditionalData is not shown in CLI output.

Tooling API Clients

Tooling API clients can access reported problems tied to failures:

  • Register a progress listener for OperationType.ROOT.

  • Check if the result is a FailureResult and retrieve problems with Failure.getProblems().

Alternatively, configure the project connection with LongRunningOperation.withFailureDetails(). Then failure details, including problems, are automatically available via GradleConnectionException.getFailures().

Generated HTML Report

Gradle generates an HTML report at the end of the build if any problems were reported. It’s a central place to review issues and complements the console and Build Scan outputs.

To keep stack trace capture affordable, Gradle infers a source location for only a limited number of problems per build, so very large builds may list some problems without a file and line. Run with --warning-mode=all or --warning-mode=fail to infer a location for every problem; past the cap, the trace is reduced to the originating build logic and calling script.

  • The report is skipped if no problems are found.

  • Disable report generation with the --no-problems-report flag.

Console output includes a link to the report, see the example output above.

problems report html

AdditionalData is not included in the HTML report. Plugins can log their own problem events, which appear alongside Gradle’s built-in reports.

Console rendering

All problem reports are now rendered in the console output if --warning-mode=all is configured.