skip to content

Inside a Karate feature invoked with `* call read('helper.feature') { id: 5 }`, what do the `__arg` and `__loop` variables hold?

level: middleimportance: nice to knowfreq 30%

answer

  1. Two bindings only a callee sees
  2. One holds what was passed
  3. One says which iteration
  4. What is its value outside a loop?

basics

~20 s

__arg holds the whole call argument, here the map { id: 5 }, or null when none was passed. __loop holds the zero-based iteration index when the feature is called once per array element, and -1 otherwise.

solid answer

~40 s

They are the two **call bindings** Karate injects into a called feature. `__arg` is the entire argument the caller passed - the map `{ id: 5 }` here - and it is `null` when the call carried no argument rather than being undefined. `__loop` is the **zero-based** index of the current iteration when an array argument turned the call into one run per element, and **-1** for an ordinary single call, which is the cheap test for "am I being looped?". The argument's keys are *also* spread as ordinary variables, so `id` is directly usable and `__arg.id` is rarely needed. Both bindings are hidden: they are readable inside the callee but never appear in the result map an isolated call returns, and they never overwrite a caller variable.

code

gherkin · 11 lines
gherkin
Feature: reusable helper

Scenario:
  # guard: fail early if the caller forgot the argument
  * match __arg != null
  # the argument's keys are also plain variables
  * def uniqueName = name + '-' + __loop
  * path 'users'
  * request { name: '#(uniqueName)' }
  * method post
  * status 201

go deeper

for a junior

Know the two names and what each holds. In everyday helpers you will use the spread argument keys directly and may never type __arg at all.

for a middle

Explain that the argument arrives twice - whole as __arg and spread as variables - and that __loop is zero-based with -1 meaning this is not a looped call.

for a senior

Use them as guardrails: assert on __arg at the top of shared helpers so a missing argument fails at the call site instead of surfacing as a null far downstream.

for a principal

Decide whether reusable features declare their inputs explicitly at all. A convention of asserting the argument shape at the top of every helper turns an implicit contract into a checked one.

## The two bindings a called feature gets for free When Karate runs a feature because another feature called it, the callee is given two variables it did not declare: - **`__arg`** - the whole call argument, exactly as the caller passed it. For `* call read('helper.feature') { id: 5 }` that is the map `{ id: 5 }`. When the caller passed nothing, `__arg` is `null`. It is deliberately bound to `null` rather than left undefined, so reading it is safe. - **`__loop`** - the zero-based index of the current iteration when the call is being looped over an array argument, and **-1** when it is not. `* match __loop == -1` is the idiomatic way for a helper to notice it is running as a single call. They are available in both shared and isolated calls, and in a feature called from a `Background` just as in one called from a step. ## The argument arrives twice This is the part worth internalising. Karate does two things with a map argument: 1. It binds the whole map to `__arg`. 2. It spreads the map's **keys as ordinary variables** in the called feature. So a helper invoked with `{ id: 5 }` can simply write `* path 'items', id` and never mention `__arg` at all. That is the normal style, and `__arg` earns its keep only when the helper wants to inspect the argument as a whole - counting keys, passing it straight through to a nested call, or matching it in one step: ```gherkin Feature: helper Scenario: * match __arg == { id: 5 } * path 'items', id * method get ``` Those spread keys are counted as the callee's **own** bindings, which is why they reappear in the map an isolated call returns. `* def r = call read('helper.feature') { id: 5 }` gives you `r.id == 5` even if the helper never touched `id`. ## Where `__loop` comes from An array argument turns one call into a run per element: ```gherkin * def rows = [{ name: 'ann' }, { name: 'bob' }] * def results = call read('create.feature') rows ``` `create.feature` runs twice. On the first run `__loop` is 0 and `__arg` is `{ name: 'ann' }`; on the second, 1 and `{ name: 'bob' }`. A helper can therefore build per-iteration values without the caller passing an index: ```gherkin * def uniqueName = name + '-' + __loop ``` Because the index is zero-based, the count is `__loop + 1` - an off-by-one that shows up in generated identifiers more often than anywhere else. | How the feature was called | `__arg` | `__loop` | |---|---|---| | `call read('h.feature')` | `null` | `-1` | | `call read('h.feature') { id: 5 }` | `{ id: 5 }` | `-1` | | `call read('h.feature') rows`, second element | that element | `1` | The same values apply whether the call was shared or isolated, and whether it came from the `call` step or from `karate.call`. ## They are hidden, and that matters Both bindings are injected as hidden variables. Three consequences follow: - They **never appear in the result map** of an isolated call. `r.__arg` is undefined. - They **never overwrite** a caller variable, and they do not travel back into the caller in shared scope either. - They do not show up in the variables view of a debugger or in the variable dumps in a report, which is exactly why people forget they exist. ## Practical uses 1. **Guarding a helper.** `* match __arg != null` at the top of a reusable feature turns a forgotten argument into a clear failure instead of a null-pointer style error twenty lines later. 2. **Distinguishing a looped run.** Branch on `__loop == -1` when a helper must behave differently as a one-shot than as a batch element. 3. **Generating unique data.** Suffixing a name or an email with `__loop` keeps a looped call's rows distinct without the caller having to number them. 4. **Passing through.** A helper that itself calls another feature can forward `__arg` unchanged rather than reconstructing it key by key. ## What they are not `__arg` is not a way to reach the caller's other variables - those are visible directly by name, because Karate always makes the caller's scope readable in a called feature. And `__loop` is not a general loop counter: it exists only for the array-argument call form, so a helper that is never called that way will always see -1.

  • If the argument's keys are already variables, when is `__arg` worth using?
    When you want the argument as a whole: matching it in one step to validate the call, counting or iterating its keys, or forwarding it unchanged to a nested call. For reading a single value the spread variable is shorter and clearer.
  • Does `__arg` come back in the result map of an isolated call?
    No. Both call bindings are injected as hidden variables, so they are readable inside the callee but excluded from the returned map and never propagated into the caller. The argument's spread keys do come back, because those are ordinary variables.

saying these in an interview costs you the question

  • Expecting __arg to be undefined rather than null without an argument
  • Thinking __loop starts at 1 for the first iteration
  • Believing __arg is how a callee reaches caller variables
  • Expecting __arg or __loop keys in the returned result map
  • Assuming the argument keys are not also plain variables