Files
yaak-mountain-loop/crates-cli/yaak-cli/skills/use-yaak/references/chaining.md
T
Gregory Schier 314f08545c Add a Yaak CLI skill for coding agents, installed by yaak agent install
Teaches agents to drive the CLI: workspaces, environments, requests,
sending, response chaining, and importing. SKILL.md stays lean with
four references loaded on demand.

The skill is embedded in the binary and written to ~/.agents/skills plus
any detected tool directory, so it ships with the CLI and refreshes on
update. Reinstalls keep files edited locally unless --force.

Also fixes the `request schema http` hint for URL path parameters, which
said to omit the leading colon. Names without the colon are sent as query
string parameters instead, leaving the placeholder literal in the path.
2026-08-13 19:26:44 -07:00

4.9 KiB

Chaining requests

The usual way to feed one response into the next request is a shell pipeline: send, pipe through jq, stash in a variable, interpolate into the next command. Yaak does not need that. A request can reference another request's response directly, and Yaak resolves the dependency at send time — including sending the upstream request first if it has to.

This is the highest-leverage thing the CLI offers, because the chain is stored in the workspace. The user can re-run it from the app, and it keeps working after the shell session is gone.

Reading a value out of another response

${[ response.body.path(request='rq_login', path='$.token') ]}

request is the upstream request's ID. path is JSONPath for JSON responses and XPath for XML. So a login-then-call-the-API pair is two requests and no glue:

yaak request create wk_abc123 --json '{
  "name": "Login",
  "method": "POST",
  "url": "${[ base_url ]}/auth/login",
  "bodyType": "application/json",
  "body": {"text": "{\"user\":\"demo\",\"pass\":\"${[ password ]}\"}"},
  "headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
}'
# -> Created request: rq_login

yaak request create wk_abc123 --json '{
  "name": "List Orders",
  "method": "GET",
  "url": "${[ base_url ]}/orders",
  "authenticationType": "bearer",
  "authentication": {"token": "${[ response.body.path(request='rq_login', path='$.token') ]}"}
}'

Sending "List Orders" now sends "Login" first when it needs to, extracts $.token, and puts it in the Authorization header.

Note the quoting: template function arguments use single quotes, so inside a single-quoted shell string write the payload to a file, or escape as above, or switch the outer shell quoting to double quotes and escape the inner JSON. Writing the JSON payload to a file and using --json "$(cat payload.json)" is the least error-prone for anything complex.

Controlling when the upstream request re-sends

The behavior argument decides whether the dependency is actually sent:

behavior Meaning
smart (default) Send only if there is no stored response yet
always Send every time
ttl Send if the newest response is older than ttl seconds (0 never expires)
${[ response.body.path(request='rq_login', path='$.token', behavior='ttl', ttl='300') ]}

smart is right for a token you fetch once. ttl matches a real token lifetime and is usually the best choice for auth. always is for values that must be fresh on every call, like a nonce.

Other response accessors

${[ response.header(request='rq_login', header='X-Request-Id') ]}
${[ response.body.raw(request='rq_login') ]}

response.body.path also accepts behavior/ttl, and has an alias of plain response.

Other useful template functions

These come from bundled plugins and work anywhere a value is rendered:

Function Use
uuid.v4(), also v1, v3, v5, v6, v7 Idempotency keys, unique record names
timestamp.unix(), timestamp.unixMillis(), timestamp.iso8601() Timestamps in bodies or signatures
timestamp.format(...), timestamp.offset(...) Formatted or relative times
random.range(min='1', max='100', decimals='0') Sample data
hash.sha256(input='…', encoding='hex') Digests — also md5, sha1, sha512
hmac.sha256(input='…', key='…', encoding='hex') Signed request signatures
base64.encode(input='…'), base64.decode(...) Encoded values
url.encode(input='…'), url.decode(...) Escaping values for URLs
fs.readFile(path='/abs/path', trim='true') Pull a value from a file on disk
cookie.value(name='session') Read a cookie from the jar
1password.item(...) Fetch a secret from 1Password rather than storing it

Note the shapes that are easy to misremember: the encoders are base64.encode and url.encode, not encode.base64; the random function is random.range, not random.number; and there is no timestamp.now. Argument names vary per function, and a wrong name renders as an error rather than an empty string, so check the function in the app's template editor when unsure.

When to chain and when not to

Chain when the dependency is part of the API's real shape: log in, then call; create a resource, then fetch it by the returned ID. The workspace becomes a runnable description of the API, which is the point.

Do not chain to smuggle in shell logic. If a value needs a conditional, a computation, or a retry, do that in the shell and set an environment variable. And do not build a long chain just to run a group of requests — yaak send <fl_id> already sends every request in a folder sequentially, with --fail-fast to stop at the first failure and --parallel when order does not matter. Reach for a chain when a request genuinely depends on another's response, not merely when it should run after it.