How do default values for task inputs work?

I am wondering how default values should be used in task inputs for custom Gradle tasks. Suppose, I have the following minimal code example:

abstract class EchoMessageTask: DefaultTask() {

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

    init {
        message.convention("world") // set default value for property

    fun echoMessage() {
        println("Hello ${message.get()}!")

It uses the following minimal implementation of a plugin that connects the extension with the input of the task:

class SimplePlugin: Plugin<Project> {

    override fun apply(target: Project) {
        val extension = target.extensions.create("simple",
        target.tasks.register("echoMessage", { task ->

abstract class SimplePluginExtension {
    abstract val message: Property<String>

I would expect this task to execute and print Hello world. However, it fails with the following error message:

Execution failed for task ':echoMessage'.
> Cannot query the value of task ':echoMessage' property 'message' because it has no value available.
  The value of this property is derived from:
    - extension 'simple' property 'message'

(This is Gradle 7.3)

The convention value is only used if no value is set, or if value is set to null.
You actually do set the value to the value of the extension property, so that is used.
So if you want a default value in that constellation, you have to set it on the extension property.

The idiomatic way also is to have the task and extension as unopinionated as possible, so they can also be reused easily without opinion and to add wiring and default values within the accompanying plugin.

Thank you for pointing that out. I somehow thought that I was connecting the properties and that this would keep the default value in tact.

Just in case you misunderstood as I was sloppy in wording. You are connecting the properties, this just does not use the default value as it counts as having a value set in the target property and the source property controls the default value.

Thanks for clarifying. That is how I understood your explanation. It is also what I misunderstood when I was originally reading the Gradle documentation about that topic.