# Recipes

> Shell pipelines for the LevelFour CLI: paging through every recommendation, aggregating with jq, and archiving a month of costs as CSV.

Source: https://docs.levelfour.ai/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_id` values and account IDs.

## Top 10 highest-savings recommendations

The biggest wins, sorted server side and projected to the id, the service and the monthly saving:

```bash
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`](/cli/output-formats#--jq) runs the filter in-process, so this recipe needs no pipe.

## Total savings grouped by service

This groups a page of recommendations by `service` and ranks the groups by total potential savings:

```bash
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'
```

> **Warning**
>
> `--page-size 100` is the maximum, so this total covers the first page of recommendations and nothing after it. To cover every page, wrap the call in the `for` loop from [Aggregate 100% savings recommendations](#aggregate-100-savings-recommendations).

## Pending recommendations for one account

Filter by account substring. The `account` field carries both the ID and the friendly name:

```bash
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"
'
```

That count and that total also cover one page. Read the note under [Total savings grouped by service](#total-savings-grouped-by-service) before you quote either number.

For bigger filtering jobs, prefer the server-side flags:

```bash
l4 recommendations list --account "123456789012 (prod)" --status pending
```

## Aggregate 100% savings recommendations

Find every recommendation with `savings_percentage == 100`, meaning the resource can be removed entirely, iterate across all pages, and present a compact column-aligned report. CloudWatch entries are common and noisy, so the pipeline rolls them into a single row.

```bash
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):**

```text
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`](/cli/commands/recommendations) for the full shape.

## Fetch full detail for the top five recommendations

Pipe the top recs to `xargs` to enrich each one with the full `view` payload:

```bash
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` cannot emit raw strings ([why](/guides/filtering-and-sorting#filtering-with---jq)), so this pipeline strips the quotes with a stand-alone `jq -r`.

Use it for an archival snapshot, or to feed a tool that needs the implementation detail.

## Monthly CSV archive

Archive the current month's costs as CSV, for BI tools that prefer flat files:

```bash
PERIOD=$(date -u +%Y-%m)
l4 export costs --period "$PERIOD" --format csv > "costs-${PERIOD}.csv"
```

> **Success**
>
> Nothing prints when this works. The file lands in the working directory as `costs-YYYY-MM.csv`, named from the `PERIOD` the first line computes.

The `--period` flag accepts `YYYY-MM` for billing-month exports. Pair with `aws s3 cp` (or `gcloud storage cp`) to push to object storage.

## How recipes are validated

The `npm run validate:recipes` script in `levelfour-docs` extracts the fenced bash blocks tagged with a `{/* recipe: <slug> */}` sentinel on this page and on [Filtering and sorting](/guides/filtering-and-sorting), runs each one through `bash -o pipefail -c`, and asserts a non-empty stdout (or zero-exit for snippets that redirect to a file). Auth resolves the same way as for any `l4` command, and the `LEVELFOUR_TOKEN` environment variable works in CI.

To add a new recipe to this page, follow the same sentinel pattern and confirm the script picks it up:

```bash
npm run validate:recipes
```

## Next

* [`l4 recommendations`](/cli/commands/recommendations): the `list` flags, the page cap and the JSON shape
* [`l4 export`](/cli/commands/export): CSV and JSON dumps of costs and recommendations
* [Output formats](/cli/output-formats): `--json`, `--jq`, `--template`, `--csv` and `--quiet`
* [`l4 diff`](/cli/commands/diff#ci-gate): gate a build when a Terraform or CloudFormation change adds too much monthly cost
* [`l4 commitments expiring`](/cli/commands/commitments/expiring): gate a build when a commitment is about to lapse
* [CI/CD integration](/guides/ci-cd#wiring-it-into-a-pipeline): the GitHub Actions and GitLab CI jobs that run the gate on a pull request
* [Filtering and sorting](/guides/filtering-and-sorting): the filter flags, the `--jq` quirks and the shorter worked queries
