Guides

Integrate l4 into your CLI

Any script, Makefile or CI runner can read LevelFour data by shelling out to the l4 binary, with no SDK in its dependency tree. --json, --jq, --template and --quiet give it machine-readable output, and the exit codes are stable enough to branch on.

For typed objects in Python, TypeScript or Go, see Integrate the SDK into your CLI.

Wire it into a runner

Install the binary

Put l4 on the runner. Installation covers Homebrew, go install, the release tarballs, and pinning a version inside a CI image.

Authenticate from a secret

LEVELFOUR_TOKEN bypasses the keychain, so a runner needs no interactive l4 auth login and nothing lands on the filesystem:

export LEVELFOUR_TOKEN="$LEVELFOUR_TOKEN"   # from a CI secret
l4 whoami                                   # smoke test

Authentication has the resolution order and the read versus read-write split.

A whoami that names your identity and organization is the proof the secret reached the runner. Exit 4 means no credential resolved at all.

Ask for machine-readable output

--json prints the same envelope the API returns. --jq filters it in process, so a runner with no jq binary still works:

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"

Output formats has the rest of the flags, and Recipes has the multi-stage pipelines.

Branch on the exit code

With -q the exit code is the only signal, which makes any command a boolean:

if l4 status -q; then
    echo "API healthy, proceeding"
else
    echo "API down, aborting"
    exit 1
fi

A Terraform cost gate wants the three-way version:

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 ;;
esac
Exit 2 is ExitIssuesFound, not a crash. A script that treats every non-zero exit as a failure reports an over-budget cost gate and a broken command identically. Branch on the code. Exit codes lists them all.

Wrap it from your own tooling

Pull a cost summary into shell variables:

cost-summary.sh
#!/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"

A cost gate and a monthly export, as targets:

Makefile
.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"

Wrap the binary in a function and parse --json:

l4_wrapper.py
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}")

Vendor the binary into a CI image

l4 is a single static Go binary, so an image needs no runtime libraries:

Dockerfile
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=""
Leave LEVELFOUR_TOKEN empty in the image. An ENV value is baked into the layers and ships with them, so inject the token at run time from your CI secret store.

Installation pins a version instead of tracking the latest release. For a complete GitHub Actions or GitLab job built on this binary, see CI/CD Integration.

Shell out or embed

Reach for the binary in scripts, Makefiles, CI runners and polyglot tooling. Reach for the SDK inside an internal CLI that wants native types. Start with l4 when you are unsure: it integrates fastest, and the SDK path stays open.

The binary reads LEVELFOUR_TOKEN, the SDKs read LEVELFOUR_API_KEY. Shelling out also gets the CLI's own argument validation, output formatting and TUI for nothing, and an embedded SDK has none of it. Integrate the SDK into your CLI compares the two in full.