Guides

GitHub Actions

Three workflows, each a whole file you copy into .github/workflows/. One comments a cost summary on every pull request, one runs a scheduled check with the Python or TypeScript SDK, and one uploads a daily report as a build artifact.

Nothing on this page uses the l4 CLI. Terraform cost estimation and diff checks inside a workflow live on CI/CD Integration.

Prerequisites

  • A LevelFour API key. Authentication covers where keys are created, the two key formats, and rotation.
  • The key stored as a GitHub Actions secret named LEVELFOUR_API_KEY.
Every request on this page reads, so a read key covers all three workflows. See Key scopes.

Comment a cost summary on a pull request

The job needs pull-requests: write, which is the only permission it asks for.

.github/workflows/cost-report.yml
name: Cost Report
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  cost-report:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Get savings summary
        id: summary
        env:
          LEVELFOUR_API_KEY: ${{ secrets.LEVELFOUR_API_KEY }}
        run: |
          RESPONSE=$(curl -s \
            -H "Authorization: Bearer $LEVELFOUR_API_KEY" \
            https://api.levelfour.ai/api/v1/recommendations/savings-by-provider)
          echo "body<<EOF" >> $GITHUB_OUTPUT
          echo "## Cloud Cost Summary" >> $GITHUB_OUTPUT
          echo "" >> $GITHUB_OUTPUT
          echo '```json' >> $GITHUB_OUTPUT
          echo "$RESPONSE" | python3 -m json.tool >> $GITHUB_OUTPUT
          echo '```' >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            const body = process.env.BODY;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body,
            });
        env:
          BODY: ${{ steps.summary.outputs.body }}
A working run leaves one comment on the pull request, headed Cloud Cost Summary, with the response JSON fenced underneath it.

Scheduled cost check

The same job runs in Python or in TypeScript, on a weekday schedule, and prints what it finds to the job log. Each SDK reads LEVELFOUR_API_KEY from the environment, so the client is constructed with no arguments. Python SDK and TypeScript SDK cover the rest of each client.

.github/workflows/cost-check.yml
name: Cost Check (Python)
on:
  schedule:
    - cron: '0 9 * * 1-5'

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - run: pip install levelfour

      - name: Run cost check
        env:
          LEVELFOUR_API_KEY: ${{ secrets.LEVELFOUR_API_KEY }}
        run: |
          python3 -c "
          from levelfour import LevelFour

          client = LevelFour()
          summary = client.recommendations.get_savings_by_provider()
          print(summary)

          overview = client.recommendations.get_overview()
          print(overview)

          costs = client.costs.get_summary()
          print(costs)
          "
.github/workflows/cost-check.yml
name: Cost Check (TypeScript)
on:
  schedule:
    - cron: '0 9 * * 1-5'

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - run: npm install levelfour

      - name: Run cost check
        env:
          LEVELFOUR_API_KEY: ${{ secrets.LEVELFOUR_API_KEY }}
        run: |
          npx tsx -e "
          import { LevelFourClient } from 'levelfour';
          const client = new LevelFourClient();
          const summary = await client.recommendations.getSavingsByProvider();
          console.log(JSON.stringify(summary, null, 2));

          const costs = await client.costs.getSummary();
          console.log(JSON.stringify(costs, null, 2));
          "

Daily report as a build artifact

The job writes costs.json and recommendations.json, then uploads both under the artifact name cost-report-<run id>.

.github/workflows/daily-cost-report.yml
name: Daily Cost Report
on:
  schedule:
    - cron: '0 9 * * 1-5'

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch cost summary
        env:
          LEVELFOUR_API_KEY: ${{ secrets.LEVELFOUR_API_KEY }}
        run: |
          curl -s \
            -H "Authorization: Bearer $LEVELFOUR_API_KEY" \
            https://api.levelfour.ai/api/v1/costs/summary \
            | python3 -m json.tool > costs.json

      - name: Fetch recommendations
        env:
          LEVELFOUR_API_KEY: ${{ secrets.LEVELFOUR_API_KEY }}
        run: |
          curl -s \
            -H "Authorization: Bearer $LEVELFOUR_API_KEY" \
            "https://api.levelfour.ai/api/v1/recommendations?page=1&page_size=100" \
            | python3 -m json.tool > recommendations.json

      - name: Upload report
        uses: actions/upload-artifact@v4
        with:
          name: cost-report-${{ github.run_id }}
          path: |
            costs.json
            recommendations.json
The recommendations request fetches page one and stops there. An organization with more recommendations than that page holds gets a truncated file and no warning, because the workflow never reads the pagination metadata the response carries. Pagination has the fields to check and how to walk the rest.

Next