| description | Capture values from a JSON body, header or status with a capture block, then reuse them as variables in later requests to chain calls. |
|---|
A request can pull values out of its response and hand them to the requests
after it, under an optional capture key: variable names mapped to JSON paths.
name: Log in
method: POST
url: '{{base_url}}/login'
capture:
auth_token: $.token
user_id: $.user.idEvery name captured this way is usable as {{name}} in every request after
this one, in file order, through exactly the substitution an environment file
feeds: there is one variable syntax, not two:
name: Fetch the user
method: GET
url: '{{base_url}}/users/{{user_id}}'
headers:
Authorization: 'Bearer {{auth_token}}'sendra run examples/capture-chain.yaml # a real capture-then-use flowFile order is real order. A request sees the captures of every request
before it and none of the captures of any request after it. A {{name}}
referenced before the request that captures it is a VariableNotFound, the
same failure as a typo, which is what it usually is.
Nothing persists. Captured values live for one sendra run or sendra test
invocation and no longer; nothing is written to disk, and a fresh process starts
with nothing captured. Running the second request of a chain on its own fails
loudly rather than quietly reusing a stale token.
What a path may select. Exactly one value, and one that has a text form to substitute:
| Selected | Captures as |
|---|---|
| a string | the string, unquoted ("ada" becomes ada) |
| a number | its value: 42 is 42, 1.50 is 1.5 |
| a boolean | true / false |
| nothing | a capture failure (the path matched no value) |
| more than one value | a capture failure (there is no single answer) |
null, array, object |
a capture failure (nothing to substitute) |
null has no text form that is not a guess between "" and null. An array or
an object has one, compact JSON, but the reason substitution is safe at all is
that a substituted value cannot change the shape of what it lands in, and
pushing {"a":1} into a URL is exactly that hazard.
Numbers going through serde_json means 1.50 in a body captures as 1.5:
the value, not the spelling. That differs from an environment file, where
port: 8080 is the string 8080 and nothing is normalised. An endpoint whose
exact digits matter should send them as a JSON string.
A bare string is not the only source. It is the default, and unchanged,
but a capture entry can also be an object naming a response header or the
status code instead of a JSON path:
capture:
auth_token: $.token # unchanged: bare string = JSON path
session_id:
header: Set-Cookie # a response header, matched case-insensitively
request_status:
status: true # the numeric status, as a stringA header: capture reads from the final response only, the one capture
always evaluates against, so with follow_redirects on, a Set-Cookie set
by an intermediate hop is not reachable this way; disable follow_redirects
to capture it from the 3xx response itself, or, for a Set-Cookie
specifically, prefer cookie_jar, which sits underneath
redirect-following rather than downstream of it, so it sees every hop
without giving up automatic redirect-following to do it. A header name that
repeats in the
response (Set-Cookie is the common case) is a capture failure rather than a
first-or-last guess, the same rule a JSON path selecting several values
already follows: a capture binds a name to one value, and silently picking
between repeats would make the same file behave differently depending on
header order a server happens to send in.
sendra run examples/capture-header-status.yaml # header and status capture, chainedA capture that produces no value is reported under the request that declared it, in the same shape as the assertions above it:
capture
✓ auth_token from `$.token`
✗ user_id from `$.user.id` — matched nothing in the response body
The captured value is not printed. Every other block shows what it compared,
so the omission is deliberate: a capture exists to carry a token or a session
id, and putting those on a terminal (a CI log, most of the time) would be a
decision the file's author never made. Nothing is hidden by it: under
sendra run the body it came from is printed in full just above, and --json
carries the values because it already carries that same body.
A failed capture is a failed check, and the run carries on. It counts
exactly as a failed assertion or a post_request throw does: visible in the
output, sendra test exits 4, and sendra run's exit code is untouched. It
happens after a response arrived (the request was sent, the server answered,
and the answer did not hold what the file said it would), which is that
category and not "could not send".
The requests after it are still sent, in keeping with every other per-request
failure in Sendra: one request's problem does not cancel its siblings. A
downstream request that needed the variable then fails on its own terms, with
VariableNotFound naming it, and that is a "never got a response", so a run
whose broken capture broke a chain exits 1, the more serious of the two, with
the original failure reported at the request that caused it:
→ Captures nothing
200 OK 3662 ms
capture
✗ auth_token from `$.json.token` — matched nothing in the response body
→ Needs it
error: no variable named `auth_token` in the active environment: no environment
file was found
→ Independent
200 OK 1771 ms
summary
3 requests: 0 passed, 1 failed, 1 without assertions, 1 no response
A capture that works is not a check. A login that captures a token and
asserts nothing is counted under without assertions, not under passed: a
capture is a dependency of the rest of the run, not an expectation about the
response, and counting it as a pass would report a check nobody wrote. The
asymmetry with the paragraph above is the point: a capture only enters the
verdict when it fails.
A capture whose name the environment file already defines is refused.
capture
✗ base_url from `$.host` — `.sendra/environments/staging.yaml` already
defines this variable; rename the capture or the environment entry
Neither value silently wins, and that is the whole reason: if the capture won,
{{base_url}} would mean the file's value in the requests before the capturing
one and the captured value in the requests after it: the same name, two
meanings, in one run, discoverable only by reading both files and counting
positions. If the environment won, the capture block would be a no-op that
still looks like it did something. Refusing says which two lines are in conflict
and costs one rename. It is also the reversible choice: this can be relaxed into
a precedence rule later, while a build that had been silently shadowing could
not be tightened. Sendra rejects duplicate request names in a collection for the
same reason.
The refused capture defines nothing, so the environment's value stands for every request in the run. The file keeps meaning what it says.
Two captures of the same name are fine, and the later one wins. That is not the same situation: both come from the same mechanism, file order fully decides which is in force at any point, and re-logging in or reading the next page's cursor is a real flow that has to be expressible.
- The block itself is never substituted. A
{{var}}in a capture path or name stays those characters. A JSON path selects which part of the response is read, exactly as it does in anassertionsblock where paths are left literal so that--envcannot silently redirect a check; and a variable name that changed with the environment could not be written as{{name}}in the request that uses it. - Scripts and captures cannot see each other. A
pre_requestscript cannot rewrite acaptureblock, apost_requestscript cannot read what was captured, and a script cannot stash a value for a later request. All three are natural extensions and none of them is here. - Assertions and captures cannot see each other either. They are evaluated independently against the same response: a failed assertion does not stop the capture beside it, and a failed capture does not fail an assertion. Both are reported, and a request that broke both is one failed request.