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.
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.