In a Karate Java runner, what does `Runner.path("classpath:api").tags("@smoke,@sanity", "~@wip")` select, and how do the comma, the tilde and the two separate arguments each combine?
answer
- Position decides the combinator
- Comma and separate argument differ
- Karate negates with one character
- It compiles to a JavaScript expression
- A parenthesis switches modes entirely
basics
~10 sThat chain runs scenarios tagged @smoke or @sanity but not @wip. Separate arguments are ANDed, a comma inside one argument means OR, and a leading tilde negates that whole argument.
solid answer
~40 sKarate compiles the varargs array into a single JavaScript selector expression, and the three combinators are positional rather than keywords. **Each separate argument is ANDed** with the others; **a comma inside one argument is OR**, becoming `anyOf('@smoke','@sanity')`; and **a leading `~` negates** that argument, becoming `not('@wip')`. So the example compiles to `anyOf('@smoke','@sanity') && not('@wip')`. The tilde is Karate's own negation form — there is no `not` keyword to type in the argument. If you need something the positional form cannot express, you can pass a raw expression built from `anyOf()`, `allOf()`, `not()` and `valuesFor()` instead; Karate detects the `(` and hands the string to its JavaScript engine untouched.
code
java · 14 lines// @smoke OR @sanity, and not @wip
Runner.path("classpath:api")
.tags("@smoke,@sanity", "~@wip")
.parallel(4);
// exclude two tags: two arguments, not one comma list
Runner.path("classpath:api")
.tags("~@wip", "~@flaky")
.parallel(4);
// raw expression form - note the whole condition is inside it
Runner.path("classpath:api")
.tags("anyOf('@smoke') && valuesFor('@team').isAnyOf('payments')")
.parallel(4);go deeper
Recall the three signs: comma means or, a separate argument means and, a leading tilde means not. That covers nearly every runner you will read.
Explain that Karate compiles the array into a JavaScript expression built from anyOf, allOf and not, and that a parenthesis in any argument switches the whole call to raw-expression mode.
Watch for the silent failure modes in review: a tilde spanning a comma list, or a parenthesised argument mixed with plain ones, both change the selected set with no error anywhere.
Decide how much selection belongs in compiled tag arguments versus supplied per job, since a tag vocabulary hard-coded across many runner classes becomes a rename nobody can perform.
## Three combinators, all positional `tags(String...)` on the Karate `Runner.Builder` takes any number of expressions, and the meaning of each character is decided by *where* it sits: | Form | Compiles to | Meaning | |---|---|---| | `tags("@smoke")` | `anyOf('@smoke')` | must carry `@smoke` | | `tags("@smoke,@sanity")` | `anyOf('@smoke','@sanity')` | **OR** — either tag will do | | `tags("~@wip")` | `not('@wip')` | **NOT** — must not carry `@wip` | | `tags("@smoke", "~@wip")` | `anyOf('@smoke') && not('@wip')` | **AND** across arguments | | `tags("@smoke,@sanity", "~@wip")` | `anyOf('@smoke','@sanity') && not('@wip')` | the example | So the answer to the question in the stem is: **scenarios carrying `@smoke` or `@sanity`, minus any that also carry `@wip`.** There are no words to memorise, which is the point of contrast with tools that spell their tag expressions out in English. Karate's negation is the tilde and only the tilde; writing `tags("not @wip")` selects nothing useful, because the whole string is treated as a tag name to look for. ## What the compiled selector actually is The compiled string is **JavaScript**, evaluated once per scenario against that scenario's effective tags (feature-level tags are merged into every scenario below them). Four functions are bound into that evaluation: - **`anyOf('@a','@b')`** — true when the scenario carries at least one of them. - **`allOf('@a','@b')`** — true when it carries all of them. - **`not('@a')`** — the negation the tilde compiles to. - **`valuesFor('@name')`** — reaches into a *valued* tag such as `@team=payments`, returning a helper with `isPresent`, `isAnyOf(...)`, `isAllOf(...)` and `isOnly(...)`. Because it is real JavaScript, `&&`, `||` and `!` all work between those calls. ## The escape hatch — and its trap You are not confined to the positional form. If **any** argument contains an opening parenthesis, Karate treats the array as already being an expression and uses it verbatim instead of translating anything: ```java Runner.path("classpath:api") .tags("anyOf('@smoke') && valuesFor('@team').isAnyOf('payments')") .parallel(4); ``` That unlocks combinations the shorthand cannot reach — value-bearing tags, an `allOf` requirement, nested boolean logic. The trap is what happens when you **mix** the two styles. The detection loop returns the *first* argument containing a `(` and discards the rest of the array. So: ```java .tags("@smoke", "anyOf('@a','@b')") // selects only anyOf('@a','@b') ``` silently drops `@smoke`. Nothing is logged as an error, the suite runs, and it runs the wrong set. **Pick one style per call.** If you need an expression, put the whole condition inside it. ## Ordering and other quiet edges - **Order does not matter.** `tags("~@wip", "@smoke")` and `tags("@smoke", "~@wip")` compile to the same conjunction. - **Whitespace inside a comma list is trimmed**, so `"@smoke, @sanity"` behaves like `"@smoke,@sanity"`. - **A tilde applies to its whole argument**, not to each comma-separated item. `"~@wip,@flaky"` does not mean "neither" — it negates the literal text `@wip,@flaky`, which is not a tag any scenario carries. To exclude two tags, pass two arguments: `tags("~@wip", "~@flaky")`. - **Repeated calls accumulate.** `tags("@smoke").tags("~@wip")` builds the same two-element array as one call with both. - **A tag with a value is matched on its full text.** `~@lock=frames` negates that exact spelling; use `valuesFor` when you want to reason about the value. ## Where the selector does *not* reach Two behaviours are decided **before** the selector is evaluated at all, which is why they cannot be expressed in it: 1. **`@ignore` and `@setup` scenarios are excluded unconditionally** at the top level. No tag expression selects them back in. 2. **`@env=` and `@envnot=` are checked against the runner's `karateEnv(...)` value**, not against the selector — an `@env=qa` scenario is skipped when the environment is anything else, regardless of what you asked for by tag. Knowing that boundary is what turns a memorised syntax table into an actual understanding of how the runner picks work. ## When the selector matches nothing A tag name that no scenario carries is **not an error**. `tags("@smok")` compiles cleanly to `anyOf('@smok')`, matches nothing, and the suite finishes reporting zero scenarios — green, instant, and completely useless as evidence. Three things make that mistake survivable: 1. **Read the summary line.** A run that reports `scenarios: 0` after a tag change is the signal; treat a suspiciously fast green as a failure until you have seen a non-zero count. 2. **Keep the tag vocabulary small and spelled once.** A dozen runner classes each hard-coding a string is a dozen places a rename has to reach, and nothing tells you when one is missed. 3. **Check the compiled selector, not your intent.** The translation is mechanical, so reading the argument back as `anyOf(...) && not(...)` catches a misplaced comma or tilde faster than re-running the suite does. The same silence applies to a tag you *meant* to exclude: `~@wp` for `~@wip` excludes nothing and the flaky scenarios you thought were filtered out run anyway.
- How do you exclude both `@wip` and `@flaky` in one Karate runner?Pass them as two separate arguments: `tags("~@wip", "~@flaky")`. Separate arguments are ANDed, giving `not('@wip') && not('@flaky')`. Writing `tags("~@wip,@flaky")` does not work — the tilde negates the whole argument, so Karate looks for a single tag literally spelled `@wip,@flaky` and excludes nothing.
- What happens if you mix a plain tag and a parenthesised expression in one `tags(...)` call?The parenthesised argument wins and the others are silently discarded. Karate scans the array for an opening parenthesis and, on the first hit, uses that string alone as the selector. Nothing warns you, so the suite runs the wrong subset. Keep one style per call and put the entire condition inside the expression.
- How do you select on a tag that carries a value, such as `@team=payments`?Use the expression form with `valuesFor`: `tags("valuesFor('@team').isAnyOf('payments')")`. The shorthand matches on a tag's full literal text, so `@team=payments` and `@team=search` are simply two different tag strings; `valuesFor` is what parses the value side and gives you `isPresent`, `isAnyOf`, `isAllOf` and `isOnly`.
saying these in an interview costs you the question
- Typing a word like not or and instead of the tilde
- Reading a comma as AND rather than OR
- Assuming one tilde negates every item in a comma list
- Mixing a plain tag with a parenthesised expression in one call
- Believing a tag expression can select an @ignore scenario
- Thinking argument order changes which scenarios match