skip to content

When you call onlyIf multiple times on a task, how are the predicates combined, and how can you make a skip reason visible in the logs?

level: seniorimportance: should knowfreq 35%

answer

  1. onlyIf appends → AND
  2. setOnlyIf replaces
  3. OR must live inside one predicate
  4. reason overload since 7.6
  5. reason shown under --info

basics

~20 s

Multiple onlyIf predicates are ANDed: the task runs only if all return true. To surface why it skipped, use the onlyIf(reason) { } overload (Gradle 7.6+); the reason appears in --info/--debug output when the task is skipped.

solid answer

~50 s

Each `onlyIf {}` call **adds** a predicate rather than replacing the previous one, and Gradle ANDs them — the task runs only when **every** predicate is true; the first that returns false causes a `SKIPPED` outcome. There's also `setOnlyIf {}` which **replaces** the entire set with a single predicate (useful to clear inherited ones). For observability, Gradle 7.6 added overloads that take a human-readable reason: `onlyIf("reason") { ... }` and `setOnlyIf("reason") { ... }`. When the predicate evaluates false, the reason is recorded and shown in `--info`/`--debug` logs (e.g. 'Skipping task ... as task onlyIf is false: <reason>'), which is invaluable for debugging why a CI task was skipped. Without the reason overload, you only see `SKIPPED` with no explanation. Predicates receive the task instance, so they can branch on task state when ANDing several conditions.

code

kotlin · 5 lines
kotlin
tasks.register("release") {
    onlyIf("on main branch") { branch() == "main" }
    onlyIf("CI build") { System.getenv("CI") != null }
    // runs only if main AND CI; --info shows which reason failed
}

go deeper

for a junior

Know that adding more onlyIf makes the task harder to run (all must pass).

for a middle

Explain AND semantics and the existence of a reason overload for logging.

for a senior

Distinguish onlyIf (append/AND) from setOnlyIf (replace), express OR correctly, and use reasons for observability.

for a principal

Mandate reason strings in shared build logic and govern when overriding plugin-contributed predicates with setOnlyIf is acceptable.

## Adding vs replacing predicates `Task.onlyIf(Spec)` **appends** a predicate. Internally Gradle keeps an AND-combined spec; calling it N times produces a logical AND of all N predicates. So: ```kotlin tasks.register("x") { onlyIf { a() } // predicate 1 onlyIf { b() } // predicate 2 — ANDed with 1 } // runs only if a() && b() ``` If any predicate returns false, the task is `SKIPPED`; Gradle does not necessarily evaluate the rest once one is false (short-circuit semantics for the run/skip decision). `Task.setOnlyIf(Spec)` **replaces** the whole accumulated spec with a single predicate. Use it to discard predicates added by a plugin or a convention you don't want. ## Making the skip reason visible Before Gradle 7.6 a skipped task just printed `> Task :x SKIPPED` with no explanation, which made debugging painful. Gradle 7.6 added reason-carrying overloads: ```kotlin tasks.register("deploy") { onlyIf("DEPLOY_ENV must be 'prod'") { System.getenv("DEPLOY_ENV") == "prod" } } ``` When the predicate is false and you run with `--info`, Gradle logs something like: ``` Skipping task ':deploy' as task onlyIf is false: DEPLOY_ENV must be 'prod' ``` This turns an opaque SKIPPED into a self-documenting decision, which is the recommended practice for non-obvious gates in shared build logic. ## Practical guidance - Use multiple `onlyIf` calls (or one combined predicate) to compose conditions; remember it's AND, not OR — for OR, combine inside one predicate. - Always attach a **reason** for non-trivial gates so CI logs explain skips. - Use `setOnlyIf` deliberately when you must override predicates contributed elsewhere; otherwise prefer additive `onlyIf` to avoid clobbering a plugin's logic.

  • You want the task to run if either of two conditions holds. How do you express OR?
    Put the OR inside a single onlyIf predicate: onlyIf { condA || condB }. Separate onlyIf calls AND together, not OR.
  • A plugin added an onlyIf you must override entirely. What do you call?
    setOnlyIf { ... } — it replaces the accumulated predicate rather than ANDing a new one.
  • Where do you see the onlyIf reason string?
    In --info/--debug logs when the task is skipped (e.g. 'Skipping task ... as task onlyIf is false: <reason>').

saying these in an interview costs you the question

  • Claiming multiple onlyIf calls OR together — they AND.
  • Thinking a second onlyIf replaces the first — it appends; only setOnlyIf replaces.
  • Assuming the reason shows at default log level — it appears at --info/--debug.

context