Recipes
Every recipe on this page is validated against the live API by npm run validate:recipes in CI. If a recipe stops working, the build fails. Substitute your own recommendation_ids, account IDs, and customer codes; the pipelines themselves are stable.
Aggregate 100% savings recommendations
Find every recommendation with savings_percentage == 100 (i.e. the resource can be removed entirely), iterate across all pages, and present a compact column-aligned report. CloudWatch entries are common and noisy, so they're aggregated into a single row.
TOTAL=$(l4 recommendations list --page-size 100 --json | jq '.data.data.pagination.total_pages')
for p in $(seq 1 $TOTAL); do
l4 recommendations list --page-size 100 --page $p --json
done \
| jq -rs '
(map(.data.data.items) | add | map(select(.savings_percentage == 100))) as $recs
| ($recs | map(select(.service != "CloudWatch"))) as $detail
| ($recs | map(select(.service == "CloudWatch"))) as $cw
| ($detail[] | [.recommendation_id, .service, (.environment // "-"), (.account // "-"),
"$" + (.monthly_savings | tostring) + "/mo", .actions.overview] | @tsv),
(if ($cw | length) > 0 then
[
"[" + ($cw | length | tostring) + " items]",
"CloudWatch",
([$cw[].environment] | unique | join(", ")),
"multiple",
"$" + ($cw | map(.monthly_savings) | add * 100 | round / 100 | tostring) + "/mo",
"\($cw | length) CloudWatch recs aggregated (run with --service CloudWatch to list)"
] | @tsv
else empty end),
"TOTAL\t\t\t\t$" + (($recs | map(.monthly_savings) | add * 100 | round / 100) | tostring) + "/mo\t(" + ($recs | length | tostring) + " items)"
' \
| column -t -s $'\t'What this does:
- Reads
total_pagesfrom the first response to know how many pages to fetch. - Fetches each page with
--jsonand concatenates the streams. jq -rsslurps all pages into a single array, filters to 100% savings recs, splits CloudWatch off into its own bucket, and emits TSV rows.column -taligns the TSV into a readable table.
Sample output (sanitized):
REC-001 S3 prod 123456789012 $9466.17/mo Convert data lake to Parquet — savings opportunity
REC-002 RDS gds 234567890123 $1293.74/mo Remove redundant cross-region backup copy
REC-003 RDS prod 123456789012 $1206.43/mo Migrate db.m5 → db.r6g (Graviton)
…
[85 items] CloudWatch multi multiple $255.00/mo 85 CloudWatch recs aggregated (run with --service CloudWatch to list)
TOTAL $3660.52/mo (114 items)JSON paths used: .data.data.pagination.total_pages, .data.data.items[].savings_percentage, .data.data.items[].service, .actions.overview. See l4 recommendations for the full shape.
Top 10 highest-savings recommendations
Quick view of the biggest wins, sorted server-side and projected to just the fields you care about:
l4 recommendations list \
--sort-by monthly_savings --sort-order desc \
--page-size 10 \
--jq '.data.data.items[] | {id: .recommendation_id, service, savings: .monthly_savings}'--jq filters in-process — no separate jq binary required.
Total savings grouped by service
Where are most of your savings concentrated? This groups the first page (up to 100 recs) by service and ranks by total potential savings:
l4 recommendations list --page-size 100 --json | jq -r '
.data.data.items
| group_by(.service)
| map({service: .[0].service, count: length, total_savings: (map(.monthly_savings) | add * 100 | round / 100)})
| sort_by(-.total_savings)
| .[]
| [.service, (.count | tostring), "$" + (.total_savings | tostring) + "/mo"] | @tsv
' | column -t -s $'\t'To aggregate across all pages, wrap in the same for loop pattern as the first recipe.
Pending recommendations for one account
Filter by account substring (matches against the account field which contains both ID and friendly name):
l4 recommendations list --page-size 100 --json | jq -r '
[.data.data.items[]
| select(.account | test("123456789012"))
| select(.status == "pending" or .status == "available")
] as $matches
| "\($matches | length) match(es) — $\($matches | map(.monthly_savings) | add)/mo"
'For bigger filtering jobs, prefer the server-side flags:
l4 recommendations list --account "123456789012 (prod)" --status pendingFetch full detail for top N recommendations
Pipe top recs to xargs to enrich each with the full view payload:
l4 recommendations list \
--sort-by monthly_savings --sort-order desc --page-size 5 \
--json \
| jq -r '.data.data.items[].recommendation_id' \
| xargs -I{} l4 recommendations view {} --json--jq always emits JSON-encoded strings (with quotes); for shell consumption we pipe through stand-alone jq -r to strip them.
Useful for archival snapshots or for feeding into downstream tooling that needs the implementation details.
Monthly CSV archive
Archive the current month's costs as CSV — useful for BI tools that prefer flat files:
PERIOD=$(date -u +%Y-%m)
l4 export costs --period "$PERIOD" --format csv > "costs-${PERIOD}.csv"The --period flag accepts YYYY-MM for billing-month exports. Pair with aws s3 cp (or gcloud storage cp) to push to object storage.
CI gate: fail if a Terraform diff adds too much cost
l4 diff --base main --fail-above 100 -q ./infra/Exit code 2 (ExitIssuesFound) is distinct from 1 (general error) — CI can branch on the difference. See l4 diff and the GitHub Actions guide.
How recipes are validated
The npm run validate:recipes script in levelfour-docs extracts every fenced bash block tagged with a {/* recipe: <slug> */} sentinel, runs it through bash -o pipefail -c, and asserts a non-empty stdout (or zero-exit for snippets that redirect to a file). Auth is resolved the same way as for any l4 command — LEVELFOUR_TOKEN env var works in CI.
To add a new recipe to this page, follow the same sentinel pattern and confirm the script picks it up:
npm run validate:recipes