skip to content

Worker API: WorkAction & WorkerExecutor

Submitting units of work to WorkerExecutor as WorkAction implementations and awaiting their completion. Interviewers ask because it is how one task parallelizes without spawning threads behind Gradle's back.

on this pageshow

questions

5

What is the Gradle Worker API, and why would a custom task use it instead of just running its work directly in the task action?

level: juniorimportance: must knowfreq 55%

answer

  1. @Inject WorkerExecutor
  2. WorkAction<WorkParameters>
  3. queue from noIsolation/classLoaderIsolation/processIsolation
  4. submit returns immediately
  5. Gradle awaits before task done

basics

~20 s

The Worker API lets a task hand units of work to Gradle's worker pool so they run in parallel. You inject WorkerExecutor, define a WorkAction, and submit work to a queue. It enables parallelism and isolation that a plain task action can't get.

solid answer

~40 s

The Worker API is Gradle's mechanism for splitting a task's work into discrete units (`WorkAction`s) submitted to a `WorkerExecutor` queue. Gradle runs those units in parallel using its build-wide worker thread/process pool, respecting `--max-workers`. Compared to running everything inline in `@TaskAction`, it gives you: (1) parallel execution of independent items even within one task; (2) isolation — `classLoaderIsolation` or `processIsolation` separate your code/deps from Gradle and other tasks; (3) better resource control because Gradle coordinates the worker budget across all running tasks. You inject `WorkerExecutor` via `@Inject`, get a queue from `noIsolation()`/`classLoaderIsolation()`/`processIsolation()`, and `submit(MyAction::class) { params... }`. The task action returns immediately; Gradle ensures the work completes before the task is done.

code

kotlin · 25 lines
kotlin
abstract class ReverseTask : DefaultTask() {
    @get:Inject abstract val workerExecutor: WorkerExecutor
    @get:InputFiles abstract val sources: ConfigurableFileCollection
    @get:OutputDirectory abstract val outputDir: DirectoryProperty

    @TaskAction
    fun run() {
        val queue = workerExecutor.noIsolation()
        sources.forEach { f ->
            queue.submit(ReverseAction::class.java) {
                it.inputFile.set(f)
                it.outputDir.set(outputDir)
            }
        }
    }
}

interface ReverseParams : WorkParameters {
    val inputFile: RegularFileProperty
    val outputDir: DirectoryProperty
}

abstract class ReverseAction : WorkAction<ReverseParams> {
    override fun execute() { /* do the work */ }
}

go deeper

for a junior

Know the three pieces: inject WorkerExecutor, implement WorkAction, submit to a queue for parallel work.

for a middle

Explain the parameters-via-WorkParameters mechanism and that submit is non-blocking with Gradle awaiting automatically.

for a senior

Contrast inline task work vs Worker API in terms of parallelism, isolation, and respecting the build-wide worker budget.

for a principal

Frame it as the supported concurrency primitive for plugins — why Gradle owns the pool, and how it interacts with --max-workers and the configuration cache.

## What the Worker API is The **Worker API** is Gradle's supported way to run a task's work as independent **units of work** on a shared worker pool, instead of doing it all inline inside a `@TaskAction` method. It is the recommended replacement for spinning up your own threads/executors inside a task — Gradle owns the thread/process budget and coordinates it across the whole build. Two core types: - **`WorkerExecutor`** — a service Gradle injects into your task. You ask it for a **`WorkQueue`** with a chosen isolation mode. - **`WorkAction<P : WorkParameters>`** — an interface you implement with a single `execute()` method. It is the unit of work. Its parameters arrive via a typed `WorkParameters` object. ## Why not just run inline? A plain task action runs serially on the task's own thread. The Worker API adds three things: 1. **Parallelism within a task.** If a task processes 200 files, each file can be a `WorkAction` and Gradle runs them across worker threads up to `--max-workers`. 2. **Isolation.** `classLoaderIsolation()` runs the action in an isolated classloader (its own classpath, no leakage of Gradle internals); `processIsolation()` runs it in a separate JVM (own heap, JVM args). This is how, e.g., annotation processors or external tools run without polluting the build JVM. 3. **Coordinated resource use.** Because all tasks submit into the same pool, Gradle keeps total concurrency within the worker limit instead of every task spawning its own threads. ## The flow ```kotlin abstract class ReverseTask : DefaultTask() { @get:Inject abstract val workerExecutor: WorkerExecutor @get:InputFiles abstract val sources: ConfigurableFileCollection @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun run() { val queue = workerExecutor.noIsolation() sources.forEach { f -> queue.submit(ReverseAction::class.java) { it.inputFile.set(f) it.outputDir.set(outputDir) } } // task action returns; Gradle awaits the queue before the task finishes } } interface ReverseParams : WorkParameters { val inputFile: RegularFileProperty val outputDir: DirectoryProperty } abstract class ReverseAction : WorkAction<ReverseParams> { override fun execute() { val src = parameters.inputFile.asFile.get() // ... do the work } } ``` Key points: the parameters interface uses Gradle's lazy types (`Property`, `RegularFileProperty`, etc.); the `WorkAction` must be `abstract` (Gradle generates the `parameters` accessor); and `submit` returns immediately — the task action does not block on each unit. ## When the work completes Work submitted to a queue is asynchronous from the caller's view, but Gradle guarantees all submitted units finish before the **task** is considered complete — you don't normally need to wait explicitly. (You can force a wait mid-action with `await()`, covered separately.)

  • Does submitting to a no-isolation queue actually run work in parallel?
    Yes — even noIsolation work runs on Gradle's worker threads and can execute concurrently with other submitted units, up to --max-workers. The only thing noIsolation skips is classloader/process separation, not parallelism.
  • How do parameters get into the WorkAction?
    Through a typed interface extending WorkParameters whose properties are Gradle lazy types. You set them in the submit { } block; Gradle serializes/snapshots them and exposes them via the action's generated parameters property.

Think of the task action as a foreman handing slips of work to a shared crew (Gradle's worker pool) rather than doing every job personally — the crew is sized once for the whole site, not per foreman.

saying these in an interview costs you the question

  • Saying the Worker API is only for separate processes — noIsolation and classLoaderIsolation also exist and run in-JVM.
  • Claiming you must call await() to make work run — Gradle awaits the queue automatically before the task finishes.
  • Spawning raw Java threads/ExecutorService inside a task instead of using the worker pool.

context

open as a page

How do you pass data into a WorkAction, and what are the constraints on those parameters?

level: middleimportance: must knowfreq 45%

basics

~20 s

Define an interface extending WorkParameters with Gradle lazy-typed properties (Property, RegularFileProperty, etc.). Set them in the submit { } block. The action reads them via its parameters property. Values must be isolatable so Gradle can snapshot them.

open as a page

Explain the difference between noIsolation(), classLoaderIsolation(), and processIsolation() when obtaining a WorkQueue. When would you choose each?

level: seniorimportance: must knowfreq 50%

basics

~20 s

noIsolation runs in the build JVM with the plugin's classpath. classLoaderIsolation runs in a separate classloader so deps don't leak. processIsolation runs in a forked JVM with its own classpath, heap, and JVM args. More isolation costs more startup overhead.

open as a page

How does the Worker API surface failures from a WorkAction, and how should a plugin author handle errors across submitted units?

level: middleimportance: should knowfreq 30%

basics

~20 s

An exception thrown in a WorkAction.execute() is captured by Gradle and rethrown to the build as a WorkerExecutionException, failing the task. With multiple failing units, Gradle aggregates them. The failure surfaces at await() if you call it, otherwise at task end.

open as a page

What does WorkQueue.await() do, when is it actually needed, and what are the downsides of calling it inside a task action?

level: seniorimportance: should knowfreq 35%

basics

~20 s

await() blocks the task action until all work submitted to that queue finishes, and rethrows any worker failures as a WorkerExecutionException. You rarely need it because Gradle awaits automatically at task end; use it only when later steps in the same action depend on the work's output.

open as a page