Transforms shape API responses before your agent sees them. Instead of parsing raw 50-field JSON blobs, your agent gets clean, focused output.
pre-transform -> HTTP request -> assert -> transform -> output
Transforms run in a pipeline. Each step receives the output of the previous step. Pre-transforms modify the request before it's sent. Assertions validate the response. Transforms shape the output.
Raw API response (47 fields):
{
"coord": {"lon": -0.13, "lat": 51.51},
"weather": [{"id": 800, "main": "Clear", "description": "clear sky"}],
"main": {"temp": 18.5, "feels_like": 17.2, "humidity": 72, "pressure": 1013},
"wind": {"speed": 4.12, "deg": 250},
"sys": {"country": "GB"},
"name": "London"
}After transforms:
Temperature: 18.5C (feels like 17.2C)
Humidity: 72%
Wind: 4.12 m/s
The spec that does this:
actions:
- name: current
url: https://api.openweathermap.org/data/2.5/weather
params:
- name: q
required: true
in: query
transform:
- type: json
extract: "$.main"
select: [temp, feels_like, humidity]
- type: template
template: |
Temperature: {{.temp}}C (feels like {{.feels_like}}C)
Humidity: {{.humidity}}%Pull a subset of data from a nested JSON response using JSONPath.
transform:
- type: json
extract: "$.data.items"Supports dot notation and array indexing: $.results[0].name, $.data.users.
Keep only specific fields from objects or arrays of objects.
transform:
- type: json
select: [name, email, role]Rename fields for clarity.
transform:
- type: json
rename:
dt: date
temp_max: high
temp_min: lowLimit array length or string length.
transform:
- type: truncate
max_items: 10 # keep first 10 array items
max_length: 500 # truncate strings to 500 charsFormat output using Go templates.
transform:
- type: template
template: |
{{range .}}- {{.name}}: {{.status}}
{{end}}Convert HTML content to clean markdown. Essential for website specs.
transform:
- type: html_to_markdown
remove_images: true
remove_links: falseCustom JavaScript transform (sandboxed, no network access).
transform:
- type: js
script: |
function transform(data) {
return data.items.map(item => ({
name: item.name,
score: item.stars * 2 + item.forks
}))
}The script must define a transform(data) function. No fetch, eval, require, or timers. 5-second timeout.
Prepend text to the output.
transform:
- type: prefix
value: "Results for {{.query}}:"Filter items matching a condition.
transform:
- type: only
filter: ".status == 'open'"Add static data to the output.
transform:
- type: inject
inject:
source: "github"
fetched_at: "2026-01-01"Remove sensitive fields from output.
transform:
- type: redact
patterns:
- field: "email"
replace: "[redacted]"
- field: "api_key"
replace: "****"Sort arrays by a field.
transform:
- type: sort
field: "created_at"
order: descApply a jq-style filter expression.
transform:
- type: filter
filter: '.[] | select(.language == "Go")'Annotate output with estimated token cost metadata.
transform:
- type: cost
model: "claude-sonnet-4-20250514"
input_tokens: "$.usage.prompt_tokens"
output_tokens: "$.usage.completion_tokens"Pre-transforms modify the request before it's sent. They run with on: request.
Inject default values for missing parameters.
transform:
- type: default_params
on: request
default_params:
per_page: "25"
sort: "updated"Rename parameters before sending.
transform:
- type: rename_params
on: request
rename_params:
query: q
count: per_pageBuild a request body from a Go template.
transform:
- type: template_body
on: request
template_body: |
{"query": "{{.query}}", "filters": {"status": "{{.status}}"}}Assertions validate the response. If any assertion fails, the action returns an error.
assert:
- type: status
values: [200, 201]
- type: json
exists: "$.data"
not_empty: "$.data.items"
- type: contains
value: "success"| Type | What it checks |
|---|---|
status |
HTTP status code is in the values list |
json / exists |
JSONPath field exists in response |
json / not_empty |
JSONPath field is not empty |
contains |
Response body contains the string |
js |
Custom JavaScript validation |
Transforms execute in order. Each step receives the output of the previous step.
transform:
# Step 1: Extract the nested data
- type: json
extract: "$.response.data.repositories"
# Step 2: Keep only the fields we need
- type: json
select: [name, stars, language]
# Step 3: Sort by stars
- type: sort
field: stars
order: desc
# Step 4: Limit to top 5
- type: truncate
max_items: 5
# Step 5: Format for the agent
- type: template
template: |
Top repositories:
{{range .}}- {{.name}} ({{.language}}) - {{.stars}} stars
{{end}}Use clictl transform to test pipelines on JSON data without hitting an API:
# From flags
echo '{"data": [{"name": "a"}, {"name": "b"}]}' | clictl transform --extract '$.data'
# From a YAML file
echo '{"items": [1,2,3]}' | clictl transform --file my-transforms.yaml
# Chain with a raw API response
clictl run my-tool get-data --raw | clictl transform --extract '$.results' --truncate 5Use --raw with clictl run to skip the spec's transforms and see the full API response.
See also: Spec Format | CLI Reference | Composite Actions