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

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

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

    public static final ProblemGroup PROBLEM_GROUP = ProblemGroup.create("sample-group", "Sample Group");

    private final ProblemReporter problemReporter;

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

    @Inject
    public ProblemReportingPlugin(Problems problems) { (1)
        this.problemReporter = problems.getReporter(); (2)
    }

    public void apply(Project project) {
        ProblemId problemId = ProblemId.create("adhoc-deprecation", "Plugin 'x' is deprecated", PROBLEM_GROUP);
        this.problemReporter.report(problemId, builder -> builder (3)
            .details("The plugin 'x' is deprecated since version 2.5")
            .solution("Please use plugin 'y'")
            .severity(Severity.WARNING)
            .additionalData(SomeData.class, additionalData -> {
                additionalData.setName("Some name"); (4)
            })
        );
    }
}
1 Inject the Problem service into the plugin.
2 Create a ProblemReporter for the plugin (use the plugin ID as the namespace).
3 Report a recoverable problem so the build can continue.
4 Add optional extra data to the problem.

See the full end-to-end sample for more details.

Problems API Usage

The following simple plugin adds a greet task:

  • It prints a Hello World greeting when used correctly: ./gradlew greet -Precipient=World

  • It warns if no recipient is set: ./gradlew greet

  • It fails if the recipient is fail: ./gradlew greet -Precipient=fail

The details of the failure will be provided in the HTML Problems Report:

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.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 : DefaultTask() {

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

    @get:Inject
    abstract val problems: Problems

    private val GROUP: ProblemGroup =
        ProblemGroup.create("org.example.hello-problems", "Hello Problems")
    private val WARN_ID: ProblemId =
        ProblemId.create("missing-recipient", "Recipient not set", GROUP)
    private val FAIL_ID: ProblemId =
        ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)

    @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(WARN_ID) {
                details("No recipient configured")
                severity(Severity.WARNING)
                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"), FAIL_ID) {
                details("Recipient 'fail' is not allowed")
                severity(Severity.ERROR)
                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.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
    abstract Property<String> getRecipient()

    @Inject
    abstract Problems getProblems()

    private static final ProblemGroup GROUP =
        ProblemGroup.create("org.example.hello-problems", "Hello Problems")
    private static final ProblemId WARN_ID =
        ProblemId.create("missing-recipient", "Recipient not set", GROUP)
    private static final ProblemId FAIL_ID =
        ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)

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

        // Warning: missing recipient -> provide a helpful suggestion
        if (name.isEmpty()) {
            reporter.report(WARN_ID) { spec ->
                spec.details("No recipient configured")
                    .severity(Severity.WARNING)
                    .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"), FAIL_ID) { spec ->
                spec.details("Recipient 'fail' is not allowed")
                    .severity(Severity.ERROR)
                    .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:

  1. Injects the Problems service into the task.

  2. Creates a ProblemGroup to group related problems.

  3. Creates a ProblemId for each distinct issue.

  4. Reports warnings with the ProblemReporter.report() when the build can continue.

  5. Throws structured failures with ProblemReporter.throwing() when the build must stop.

  6. Optionally attaches AdditionalData to ProblemSpec so advanced tools can show extra info.

Injecting the Problems service

Gradle will automatically provide the Problems service when injected into the custom task type:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
@get:Inject
abstract val problems: Problems
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
@Inject
abstract Problems getProblems()

Creating a ProblemId and ProblemGroup

Problem IDs and Groups help Gradle deduplicate, summarize, and link to solutions:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
private val GROUP: ProblemGroup =
    ProblemGroup.create("org.example.hello-problems", "Hello Problems")
private val WARN_ID: ProblemId =
    ProblemId.create("missing-recipient", "Recipient not set", GROUP)
private val FAIL_ID: ProblemId =
    ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)
plugin/src/main/groovy/org/example/HelloProblemsPlugin.groovy
private static final ProblemGroup GROUP =
    ProblemGroup.create("org.example.hello-problems", "Hello Problems")
private static final ProblemId WARN_ID =
    ProblemId.create("missing-recipient", "Recipient not set", GROUP)
private static final ProblemId FAIL_ID =
    ProblemId.create("forbidden-recipient", "Forbidden recipient 'fail'", GROUP)

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(WARN_ID) {
    details("No recipient configured")
    severity(Severity.WARNING)
    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(WARN_ID) { spec ->
    spec.details("No recipient configured")
        .severity(Severity.WARNING)
        .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"), FAIL_ID) {
    details("Recipient 'fail' is not allowed")
    severity(Severity.ERROR)
    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"), FAIL_ID) { spec ->
    spec.details("Recipient 'fail' is not allowed")
        .severity(Severity.ERROR)
        .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, severity, solutions, and additional data:

plugin/src/main/kotlin/org/example/HelloProblemsPlugin.kt
details("No recipient configured")
severity(Severity.WARNING)
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")
    .severity(Severity.WARNING)
    .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.

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", "Sample Error", 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 ':sample-project:myFailingTask'.
> Message from runtime exception
    This happened because ProblemReporter.throwing() was called
      This is a demonstration of how to add
      detailed information to a build failure

* Try:
> 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

Notes: - AdditionalData is not shown in CLI output. - Solutions or recommended actions in the report will appear in the “Try:” section.

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.

  • 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:

[Incubating] Problem report is available at: <project-dir>/build/reports/problems/problems-report.html

BUILD SUCCESSFUL in 1s
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.