CLI

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:

  1. Reads total_pages from the first response to know how many pages to fetch.
  2. Fetches each page with --json and concatenates the streams.
  3. jq -rs slurps all pages into a single array, filters to 100% savings recs, splits CloudWatch off into its own bucket, and emits TSV rows.
  4. column -t aligns 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 pending

Fetch 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