val scalaVersion = settingKey[String]("The version of Scala used for building.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")

The key constructors have two string parameters: the name of the key
("scalaVersion") and a documentation string
("The version of scala used for building.").

Remember from .sbt build definition that the type
parameter T in SettingKey[T] indicates the type of value a setting has.
T in TaskKey[T] indicates the type of the task’s result. Also remember
from .sbt build definition that a setting has a fixed
value until project reload, while a task is re-computed for every “task
execution” (every time someone types a command at the sbt interactive
prompt or in batch mode).

Keys may be defined in an .sbt file,
a .scala file, or in an auto plugin.
Any vals found under autoImport object of an enabled auto plugin
will be imported automatically into your .sbt files.

Implementing a task

Once you’ve defined a key for your task, you’ll need to complete it with
a task definition. You could be defining your own task, or you could be
planning to redefine an existing task. Either way looks the same; use :=
to associate some code with the task key:

If the task has dependencies, you’d reference their value using value,
as discussed in task graph.

The hardest part about implementing tasks is often not sbt-specific;
tasks are just Scala code. The hard part could be writing the “body” of
your task that does whatever you’re trying to do. For example, maybe
you’re trying to format HTML in which case you might want to use an HTML
library (you would
add a library dependency to your build definition and
write code based on the HTML library, perhaps).

sbt has some utility libraries and convenience functions, in particular
you can often use the convenient APIs in
IO to manipulate files and directories.

Execution semantics of tasks

When depending on other tasks from a custom task using value,
an important detail to note is the execution semantics of the tasks.
By execution semantics, we mean exactly when these tasks are evaluated.

If we take sampleIntTask for instance, each line in the body of the task
should be strictly evaluated one after the other. That is sequential semantics:

Because sampleStringTask depends on both startServer and sampleIntTask task,
and sampleIntTask also depends on startServer task, it appears twice as task dependency.
If this was a plain Scala method call it would be evaluated twice,
but since value is just denoting a task dependency, it will be evaluated once.
The following is a graphical notation of sampleStringTask’s evaluation:

If we did not deduplicate the task dependencies, we will end up
compiling test source code many times when test task is invoked
since compile in Test appears many times as a task dependency of test in Test.

Cleanup task

How should one implement stopServer task?
The notion of cleanup task does not fit into the execution model of tasks because
tasks are about tracking dependencies.
The last operation should become the task that depends
on other intermediate tasks. For instance stopServer should depend on sampleStringTask,
at which point stopServer should be the sampleStringTask.