skip to content

In Karate, when you write `* def result = call read('helper.feature') { id: 5 }`, what exactly does `result` hold?

level: middleimportance: must knowfreq 52%

answer

  1. It is a map of variables
  2. Not everything in scope comes back
  3. Which scenario's variables?
  4. An array argument changes the shape

basics

~10 s

A map of the variables the called feature contributed itself, taken from the last scenario it executed. Caller variables it merely inherited to read are excluded, and the call argument's keys are included.

solid answer

~40 s

`result` is a **map**, not a report object: it holds the variables the called feature defined or replaced, taken from the **last scenario that feature executed**. The caller's own variables are visible inside the callee but are filtered out of the result, so you get the callee's contribution rather than an echo of your own scope. The keys of the call argument count as the callee's own bindings, so `result.id` is 5 here even if the callee never touched it. The `__arg` and `__loop` bindings are hidden and never appear. One important shape change: if the argument is a **JSON array** instead of an object, the feature is called once per element and `result` is a **list of such maps**.

code

gherkin · 9 lines
gherkin
* def result = call read('helper.feature') { id: 5 }
# the argument's keys are the callee's own bindings
* match result contains { id: 5 }

# an array argument calls the feature once per element
* def rows = [{ name: 'ann' }, { name: 'bob' }]
* def results = call read('create-user.feature') rows
* match results == '#[2]'
* print results[1].name

go deeper

for a junior

Know that the result of an isolated call is a map keyed by variable name, and reach into it with a dot: auth.token. Print it once while learning to see what came back.

for a middle

Explain the three filters: last scenario only, the callee's own contribution only, and the argument keys included. Then explain how an array argument turns the map into a list.

for a senior

Watch for helpers that grew a second scenario - the result silently changes shape. Assert on the result map so a helper that stops producing a key fails loudly instead of leaving nulls downstream.

for a principal

Decide what a reusable feature's result is allowed to be: a documented contract of a few keys, or an open bag. The first survives refactoring of the helper; the second couples every caller to its internals.

## The result is a map of variables, not a report `* def result = call read('helper.feature') { id: 5 }` isolates the call, and what lands in `result` is a plain map whose keys are variable names. There is no status, no timing and no step list in it - execution results go to the report, and a failure inside the callee throws rather than being handed back as data. So the only question worth asking is *which* variables make it in. ## Which variables make it in Three rules decide the contents: - **Only the last executed scenario counts.** A called feature runs every scenario it contains, and the result map is built from the variables in scope at the end of the last one. If `helper.feature` has two scenarios, whatever the first one defined and the second one did not is not in the map. - **Only the callee's own contribution is included.** A called feature can read the caller's variables, so at the end of the run its full binding set echoes the caller's scope back. Karate strips that inherited scope out, leaving the variables the callee actually defined or replaced. Without this filter every call result would drag the caller's locals and config-level values along with it. - **The call argument's keys are the callee's own.** The keys of `{ id: 5 }` are set as ordinary variables inside the callee, and they are counted as its own bindings, so `result.id` is 5 even if `helper.feature` never mentions `id`. The hidden call bindings `__arg` and `__loop` are deliberately excluded - they exist for the callee to read and are never returned. ## Called features run all their scenarios This surprises people the first time a helper grows a second scenario. Tag filters that apply to a top-level run are not applied to a called feature, so a reusable file marked `@ignore` still runs every scenario when it is called. Two practical consequences: 1. A helper you intended as one routine now runs twice as much work per call. 2. The result map suddenly reflects the *second* scenario, and a variable the first one defined may be missing. Keeping a reusable feature to a single anonymous `Scenario:` avoids both, which is why upstream's own reusable features are written that way. ## An array argument changes the shape The argument is not just data - its type selects the call mode. | Argument | What runs | What `result` holds | |---|---|---| | omitted | the feature once | a map of the callee's variables | | a JSON object | the feature once, keys spread as variables | a map of the callee's variables | | a JSON array | the feature once per element | a **list** of those maps, in order | ```gherkin * def rows = [{ name: 'ann' }, { name: 'bob' }] * def results = call read('create-user.feature') rows * match results == '#[2]' * print results[1].userId ``` Writing `result.userId` against a list is the classic mistake here; the value you want is `result[0].userId`. ## Why the delta matters beyond tidiness The result map of an isolated call is also what a cached call replays and what a disk-backed cache serialises. If the map echoed the caller's whole scope, a cache entry would carry one caller's locals into every later caller, and log output would balloon. Treating the map as "what this feature produced" rather than "everything that was in scope" is what keeps a reusable feature genuinely reusable. ## Reading the result well - Unpack it immediately into named variables if the rest of the file reads better that way: `* def token = auth.token`. - Assert on it like any other JSON value - `* match result contains { id: 5 }` works, and is a cheap guard that the helper actually produced what you expect. - Do not assume a key exists because the helper defines it somewhere; check which scenario ran last. - If you find yourself unpacking six keys after every call, that helper probably wants shared scope instead, and the bare `call` form will save the unpacking. ## What is not in the map Three things people look for and never find: - **The hidden call bindings.** `__arg` and `__loop` are injected for the callee to read and are excluded from the result, so `result.__arg` is undefined. - **The caller's own variables.** They were readable inside the callee, but they are filtered out of the map precisely so a call result means "what this helper produced". - **A status.** A called feature that fails throws into the caller and fails the calling step; there is no success flag to inspect, so an assertion after the call is checking the helper's output, never whether it ran.

  • Why are the caller's own variables filtered out of the result map?
    Because the callee can read them, they would otherwise be echoed straight back. Filtering keeps the map to what the feature produced, which matters when the result is cached or logged - an echoed map would carry one caller's locals into every later reader of that cache entry.
  • The helper feature has two scenarios and the result map is missing a variable the first one set. Why?
    The result is built from the last scenario the called feature executed. Called features run all their scenarios and are not filtered by tags, so the second scenario's scope is what you get. Collapse the helper into one scenario, or return what you need from the last one.

Think of it as an itemised receipt for the call: it lists what the helper produced, not everything you were already carrying when you walked in.

saying these in an interview costs you the question

  • Expecting a pass/fail status object rather than a map of variables
  • Assuming the caller's variables are echoed back in the result
  • Reading result.field when an array argument returned a list
  • Thinking a called feature runs only its first scenario
  • Expecting __arg or __loop to appear as keys in the result