Integrate l4 into your CLI
If you don't want to add an SDK dependency to an existing CLI / script / Makefile / CI runner, just shell out to the official l4 binary. Use --json (or --jq, --template, --quiet) to get machine-readable output, parse with the tools you already have, and rely on the stable exit codes for branching.
For an SDK-based alternative that gives you typed objects in Python / TypeScript / Go, see Integrate the SDK into your CLI.
Pattern
- Install
l4on the runner (brew,go install, or a release tarball) - Authenticate via
LEVELFOUR_TOKENenv var (no interactiveauth loginneeded in CI) - Invoke
l4 ... --json(or--jqfor inline filtering,--quietfor boolean checks) - Branch on the exit code:
0success,1error,2issues found,4auth required,130interrupted
Authentication in CI
export LEVELFOUR_TOKEN="$LEVELFOUR_TOKEN" # from a CI secret
l4 whoami # smoke testThe LEVELFOUR_TOKEN env var bypasses the keychain entirely — no interactive flow, no filesystem state. See Authentication.
Boolean checks: --quiet + exit code
if l4 status -q; then
echo "API healthy, proceeding"
else
echo "API down, aborting"
exit 1
fiFor Terraform CI gates:
l4 diff --base main --fail-above 100 -q ./infra/
case $? in
0) echo "No cost impact" ;;
2) echo "Over budget by more than \$100/mo"; exit 1 ;;
*) echo "l4 diff failed"; exit 1 ;;
esac2 is ExitIssuesFound — distinct from generic failures (1).
Inline filtering with --jq
--jq runs the filter in-process — no separate jq binary on the runner:
HIGH_VALUE_RECS=$(l4 recommendations list \
--status available \
--sort-by monthly_savings --sort-order desc \
--jq '.data.data.items[0:5] | map(.recommendation_id)')
echo "$HIGH_VALUE_RECS"For complex multi-step pipelines (slurps, branches), pipe to standalone jq so each stage is debuggable. See Recipes for full examples.
Pulling cost summaries into a shell script
#!/usr/bin/env bash
set -euo pipefail
eval "$(
l4 costs summary --provider aws --json \
| jq -r '.data | "MONTHLY=\(.monthly_spending)\nFORECAST=\(.forecasted_monthly_costs)\nSAVINGS=\(.potential_savings)"'
)"
echo "AWS spend this month: \$$MONTHLY"
echo "Forecasted: \$$FORECAST"
echo "Potential savings: \$$SAVINGS"Wrapping l4 from Make
.PHONY: cost-check
cost-check:
@l4 diff --base main --fail-above 100 -q ./infra/ \
|| { echo "Cost gate failed"; exit 1; }
.PHONY: weekly-report
weekly-report:
@l4 export costs --period $$(date -u +%Y-%m) --format csv > "costs-$$(date -u +%Y-%m).csv"
@l4 export recommendations --format csv > "recs-$$(date +%Y%m%d).csv"Wrapping l4 from a Python script (no SDK)
import json
import subprocess
def l4(*args):
result = subprocess.run(
["l4", *args, "--json"],
capture_output=True,
text=True,
check=True,
)
return json.loads(result.stdout)
summary = l4("costs", "summary", "--provider", "aws")
print(f"Spend: ${summary['data']['monthly_spending']:,.2f}")This pattern is useful when you want LevelFour data inside an existing tool but adding an SDK to the dependency tree isn't worth it.
Vendoring the binary in a Docker image
l4 is a single static Go binary, so no runtime libraries to install:
FROM alpine:3
RUN apk add --no-cache ca-certificates curl \
&& curl -fsSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/levelfour_latest_linux_amd64.tar.gz \
| tar xz -C /usr/local/bin levelfour l4 \
&& chmod +x /usr/local/bin/l4 /usr/local/bin/levelfour
ENV LEVELFOUR_TOKEN=""For GitHub Actions, the GitHub Actions guide shows the canonical setup.
Trade-offs vs embedding the SDK
| Concern | Shell out to l4 | Embed the SDK |
|---|---|---|
| Language coupling | None — works in bash, Make, anywhere | Locked to Python / TS / Go |
| Adds a dependency | No (binary install) | Yes (PyPI / npm / go get) |
| Typed objects | No (you parse JSON) | Yes |
| Best for | Scripts, CI runners, Makefiles, polyglot tooling | Internal CLIs that want native types |
| Authentication | LEVELFOUR_TOKEN env var | LEVELFOUR_API_KEY env var |
| CLI's own validation, formatting, TUI | Available for free | Not available |
When in doubt, start with l4: it's faster to integrate and you can always switch to the SDK later if you need richer types. See Integrate the SDK into your CLI for that path.