BLOG/ENTERPRISE AUTOMATION
THEME
ENTERPRISE AUTOMATION

Cost reporting automation

The daily cloud-cost report that lives in a cron'd shell script nobody trusts. Here's how to turn pull -> transform -> report -> gate into an OrchStep workflow that fails loudly when spend blows past budget.

Apr 28, 2026 OrchStep Team 6 minROLE: FinOps EngineerSCALE: Enterprise
RUNNABLE DEMO
Full source for this post: blog/cost-reporting
VIEW SOURCE

Every FinOps team has the same artifact: a script that pulls yesterday's spend from Cost Explorer, rolls it up by service and team, drops a CSV in a bucket, and posts a number to Slack. It runs on a cron. It mostly works. And when spend quietly doubles on a Tuesday, the report still goes out — green, cheerful, and wrong — because the script has no concept of "too much."

The problem isn't the API calls. It's that the report and the alarm are two separate systems, and the alarm always lags. This post folds them into one OrchStep workflow: pull, transform, render, and then a hard gate that turns a budget breach into a failed run instead of a footnote.

The shape

Four steps, one of which has teeth. The gate step is a func: assert — if the day's total clears the budget, the run fails, and your scheduler treats a failed run the way it should: loudly.

orchstep.yml
name: finops
# Pull yesterday's cloud spend, normalize it, write a report, then gate:
# fail loudly if the day blew past budget so the alert can't be ignored.
defaults:
  report_date: "2026-04-28"
  budget_usd: "5000"

tasks:
  # `orchstep run report --var report_date=2026-04-28`
  report:
    steps:
      - name: pull
        func: shell
        do: echo "pulling cost-explorer rows for {{ vars.report_date }}"
        outputs:
          total: "4200"

      - name: transform
        func: shell
        do: echo "rolling {{ steps.pull.total }} USD up by service and team"

      - name: render
        func: shell
        do: echo "writing the {{ vars.report_date }} cost report ({{ steps.pull.total }} USD)"

      - name: gate
        func: assert
        args:
          condition: '{{ le (atoi steps.pull.total) (atoi vars.budget_usd) }}'
          message: "{{ vars.report_date }} spend {{ steps.pull.total }} USD exceeded the {{ vars.budget_usd }} USD budget"

The demo emits a literal total from pull so it runs anywhere. In your version, that step shells out to the Cost Explorer CLI and the outputs: block captures the number with a regex — the rest of the workflow doesn't change.

Why a gate beats a threshold check

You could keep the cron script and bolt a Slack alert onto the end. People do. The difference is what happens on a bad day:

CRON SCRIPT
TOTAL=$(aws ce get-cost-and-usage ... | jq '.total')
write_report "$TOTAL"
# alert is best-effort, runs last, swallowed if the report step already exited 0
if [ "$TOTAL" -gt 5000 ]; then
  curl -s "$SLACK_URL" -d "over budget" || true
fi
ORCHSTEP GATE
- name: gate
  func: assert
  args:
    condition: '{{ le (atoi steps.pull.total) (atoi vars.budget_usd) }}'
    message: "spend exceeded budget"

The assert is part of the run's exit status. A breach is a non-zero exit, which means your scheduler, your CI dashboard, and your on-call paging all see it without any extra wiring. The budget lives in defaults: and overrides per-team at the CLI with --var budget_usd=3000 — no second copy of the script per cost center.

See the report before it runs

Because the budget and date are real variables, a dry run shows you exactly which numbers and thresholds a scheduled run will use — before it touches the billing API:

orchstep run report --var report_date=2026-04-28 --dry-run

That's the difference between "I think the prod budget is 5000" and reading it in the resolved plan. Full tour: Previewing with Dry Run.

What you gained

Concerncron scriptOrchStep
Over-budget signalbest-effort Slack callfailed run, surfaced everywhere
Per-team budgetsa copy per cost centerone --var budget_usd= override
"What will it report?"read the script--dry-run prints the plan
Captured totalsa loose shell variablenamed step outputs:

If a single aws ce call and a Slack webhook genuinely cover you, keep them. The moment a wrong number can ship green, give the report a gate.

Where to go next

Got a cost report on a cron you don't fully trust? Give it a gate and let a bad day fail the run.

#FINOPS#COST#REPORTING#AUTOMATION
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — ENTERPRISE AUTOMATION