Walk through what Gradle does internally when it executes a single task — from the task graph to the @TaskAction.
answer
- task = ordered Action list
- doFirst -> @TaskAction -> doLast
- resolve providers at exec time
- snapshot in/out + cache check
- throw => FAILED, finalizers still run
basics
~20 sFor each task in graph order, Gradle resolves its inputs, runs up-to-date/cache checks, and if work is needed runs the task's action list — doFirst actions, then @TaskAction methods, then doLast actions — capturing outputs and recording an outcome.
solid answer
~50 sExecution walks the topologically-sorted task graph. For a single task Gradle: (1) finalizes the task's **input/output property** values (resolving any lazy `Provider`/`Property`), (2) snapshots inputs and checks **up-to-date** state and the **build cache**, (3) if execution is required, acquires the worker/project lock, runs the **action list** in order — `doFirst` actions (prepended), the typed task's `@TaskAction` method(s), then `doLast` actions (appended), (4) snapshots the produced outputs and stores them in history (and the cache if cacheable), (5) records the outcome (executed / UP-TO-DATE / etc.). If any action throws, the task is FAILED and the failure-handling policy kicks in. A task is essentially an ordered list of `Action<Task>` objects; `@TaskAction` is just the convention for the primary action on a typed task. With `--parallel`, independent tasks run on separate workers while ordering edges are honored.
code
kotlin · 6 linestasks.register("demo") {
doFirst { println("1 doFirst") }
doLast { println("3 doLast") }
}
// A typed task's @TaskAction would print "2" between them.
// Adding a second doFirst would run BEFORE the first (prepended).go deeper
Know that a task runs doFirst, then its main action, then doLast.
Describe the action list ordering and that inputs/outputs are snapshotted around execution.
Detail the full per-task sequence including provider resolution, up-to-date/cache checks, and failure semantics.
Reason about execution-time provider resolution and locking as the basis for configuration cache + safe parallel execution at scale.
## A task is an ordered list of actions Internally a Gradle task holds an ordered **action list**. Actions come from three sources, executed in this order: 1. `doFirst { }` blocks — **prepended** (last-added runs first). 2. `@TaskAction`-annotated method(s) of the typed task class — the **main** action. 3. `doLast { }` blocks — **appended**. Each is an `org.gradle.api.Action<Task>`. ## Per-task execution steps When the executor reaches a task in graph order: 1. **Resolve properties** — lazy `Provider`/`Property` and `RegularFileProperty`/`DirectoryProperty` inputs are realized to concrete values. (This is why values are read at execution time, not configuration time, when using providers.) 2. **Snapshot inputs** — fingerprint input files and `@Input` values. 3. **Up-to-date / cache check** — compare against history; consult the build cache for a cacheable task. If satisfied, skip with UP-TO-DATE / FROM-CACHE and stop. 4. **Execute actions** — under the appropriate lock, run `doFirst` → `@TaskAction` → `doLast` in order. Inside an action you can use the Worker API to fan out work. 5. **Snapshot outputs** — fingerprint produced outputs; persist to history; upload to cache if cacheable + enabled. 6. **Record outcome** — executed / UP-TO-DATE / FROM-CACHE / SKIPPED / NO-SOURCE / FAILED. ```kotlin abstract class Pack : DefaultTask() { @get:InputDirectory abstract val from: DirectoryProperty @get:OutputFile abstract val archive: RegularFileProperty @TaskAction // the MAIN action, runs at step 4 fun pack() { /* zip from -> archive */ } } tasks.register<Pack>("pack") { doFirst { logger.lifecycle("start") } // runs before @TaskAction doLast { logger.lifecycle("done") } // runs after @TaskAction } ``` ## Failure inside an action If any action throws, remaining actions of that task are abandoned, the task is FAILED, and the build's failure policy (fail-fast or `--continue`) applies. Finalizers still run. ## Parallelism and locks With `org.gradle.parallel=true`, tasks from independent project subgraphs run concurrently on worker threads. Gradle uses project/state locks to keep mutable project state safe; ordering edges (`dependsOn`, `mustRunAfter`) constrain which tasks may run together. ## Why providers matter here Because property values are finalized at step 1 (execution), using `Property`/`Provider` lets you defer reading a value until it's actually needed — supporting configuration avoidance and the configuration cache. Reading `.get()` too early (in a configuration block) defeats that.
- If you add two `doFirst` blocks, which runs first?The most recently added `doFirst` runs first — `doFirst` prepends to the action list, so later additions move to the front.
- Why are lazy Provider/Property values resolved at execution time?To support configuration avoidance and the configuration cache: deferring resolution lets values be computed only when the task actually runs, and lets Gradle skip configuring tasks that won't execute.
- What runs the primary work in a typed task vs an ad-hoc task?A typed task uses its `@TaskAction` method as the main action; an ad-hoc task registered with `tasks.register` typically does its work in `doLast`/`doFirst` blocks.
saying these in an interview costs you the question
- Saying doFirst blocks run in addition order (they prepend, so reverse).
- Claiming @TaskAction runs at configuration time.
- Asserting parallel tasks ignore ordering edges.