Budget Tracking skill for Codex

Every variance explained, owned and forecast. Codex sets up a budget tracker with a GL-to-budget-line map, imported accrual actuals and month, YTD and full-year variance under one sign convention. Material variances are flagged by both percent and amount, each gets an owner and a timing-or-permanent explanation, and the forecast changes while the approved budget stays untouched. A bundled script produces the flagged variance table from a CSV.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- budget-tracking

Installs to ~/.agents/skills/budget-tracking/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $budget-tracking, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds budget spreadsheets and runs budget-versus-actual variance analysis - department and GL-mapped budget layouts in Excel or Google Sheets, monthly actuals import from accounting exports, favourable/unfavourable variance with materiality thresholds, YTD and full-year reforecast, accrual and timing explanations, and a variance commentary for owners and leadership. Use when asked to set up a budget tracker, compare actuals to budget, explain a variance, reforecast the year, or prepare a monthly budget review pack.

Use it when

  • Month-end close is done and budget owners need a variance review
  • Building a budget tracker for a department or small company
  • Leadership asks where the year will land against budget
  • Actuals and budget disagree and nobody knows why

Use something else when

How to check the result

Mapped actuals equal the accounting P&L total for each closed month, unmapped GL accounts are zero, and every flagged variance has an owner, cause and forecast impact.

The mistake it prevents

Comparing cash payments against an accrual budget, which creates fake variances in every month with an annual prepayment.

~/.agents/skills/budget-tracking/SKILL.md

--- name: budget-tracking description: Builds budget spreadsheets and runs budget-versus-actual variance analysis - department and GL-mapped budget layouts in Excel or Google Sheets,… license: MIT ---

Budget tracking is working when actuals land in the tracker within days of month-end, every material variance has an owner and a one-line explanation that says whether it is timing or permanent, and the full-year forecast changes when reality does. A tracker that nobody explains is just a second ledger.

Budgets feed tax, audit and board processes; flag for the accountant or finance lead anything that touches accounting treatment (accruals, capitalisation, revenue recognition) instead of deciding it.

Workflow

  1. Read the approved budget (often from the financial model; see financial-modeling) and the chart of accounts. Confirm the fiscal year and currency.
  2. Map every GL account to exactly one budget line. Keep the map as its own tab.
  3. Import actuals for closed months from the accounting system export (QuickBooks, Xero, NetSuite, Sage; P&L by month as CSV).
  4. Compute monthly and YTD variance, flag material ones.
  5. Get explanations from budget owners; classify timing vs permanent.
  6. Update the full-year forecast (actuals for closed months + reforecast for the rest).
  7. Publish the review pack.

Tracker layout

TabContent
BudgetApproved budget, one row per line, months in columns; locked after approval
GL_MapGL account number, account name, budget line, owner
Actuals_RawPasted accounting export, unedited, with export date
ActualsSUMIFS of raw actuals by budget line and month via the map
VarianceMonth, YTD and full-year variance with flags
ForecastClosed months = actuals, open months = latest forecast
NotesVariance explanations: line, month, amount, cause, timing or permanent, owner

Core formulas (Excel and Google Sheets; months in row 1, budget lines in column A):

Actuals!C5     =SUMIFS(Actuals_Raw!$E:$E, Actuals_Raw!$F:$F, $A5, Actuals_Raw!$B:$B, C$1)
               (raw export with a helper column F = budget line via XLOOKUP on GL_Map)
Helper F2      =XLOOKUP(A2, GL_Map!$A:$A, GL_Map!$C:$C, "UNMAPPED")
Variance (exp) =Budget!C5 - Actuals!C5      positive = under budget = favourable
Variance (rev) =Actuals!C5 - Budget!C5      positive = above budget = favourable
Variance %     =IF(Budget!C5=0, "n/a", Var / ABS(Budget!C5))
YTD actual     =SUMPRODUCT((Actuals!$C$1:$N$1<=$B$2) * Actuals!$C5:$N5)
               (row 1 = month-start dates for one fiscal year; B2 = report month date)
Unmapped check =COUNTIF(Actuals_Raw!F:F, "UNMAPPED") = 0

Use one sign convention throughout and say it at the top of every report: “positive = favourable”.

Materiality

Flag a variance only when it is both large in percent and large in money, so tiny lines do not generate noise and big lines do not hide:

Budget sizeTypical threshold (adjust to the company)
Small company, total budget under $2M10% and $2,500
Mid-size department budgets10% and $10,000
Board-level summary5% of total or a fixed amount set by finance

Check YTD as well as the month: a series of small monthly overruns becomes material YTD.

The bundled script produces the month and YTD variance table with flags from a simple CSV (line,type,month,budget,actual; blank actual = month not closed) and exits 2 when anything is flagged:

python3 scripts/variance_report.py budget_vs_actual.csv --month 2026-09 --pct 10 --abs 5000

Explaining variances

Every flagged line gets a note with four parts: what, why, timing or permanent, and the forecast impact.

Cause typeExampleForecast treatment
TimingAnnual conference invoice paid in September, budgeted in OctoberNo full-year change
Accrual missingContractor invoice not received by closeAsk finance to accrue; no full-year change
VolumeMore hires, more seats, higher usageChange forecast for remaining months
PriceVendor price increase, FX movementChange forecast; consider renegotiation
One-offLegal settlement, emergency repairForecast unchanged after this month; note separately
MisclassificationCost booked to the wrong GL accountReclass with finance; fix the map if needed
Budget errorLine budgeted with wrong assumptionNote in forecast; do not edit approved budget

Write explanations with numbers: “Paid search +$7.5k over: CPC rose 22% in September after a competitor entered; plan to cut branded spend by $4k/month for Q4 (owner: Ana).” “Higher than expected” is not an explanation.

Reforecast

  • Never overwrite the approved budget. Forecast lives in its own tab or column set.
  • Full-year forecast = actuals to date + forecast for remaining months. Compare it to the full-year budget; this is the number leadership cares about.
  • Reforecast monthly or quarterly; record the date and main changes for each version.
  • When permanent variances accumulate, propose actions (cut, shift, request more budget) rather than only reporting.

Review pack template

# Budget review - <month> (fiscal year <FY>), actuals closed <date>
Sign convention: positive variance = favourable.

| | Month budget | Month actual | Var | YTD budget | YTD actual | YTD var | FY budget | FY forecast | FY var |
|---|---|---|---|---|---|---|---|---|---|
| Revenue | | | | | | | | | |
| Expenses | | | | | | | | | |
| Net | | | | | | | | | |

## Material variances
| Line | Var (month / YTD) | Cause | Timing or permanent | Action | Owner |
|---|---|---|---|---|---|

## Forecast changes since last month
## Decisions needed
## Data notes (unmapped accounts, pending accruals, reclasses)

Gotchas

  • Cash vs accrual: comparing cash payments to an accrual budget creates fake variances in months with annual prepayments. Use accrual actuals from the P&L, not bank data.
  • Late close: a month reported before accruals are booked shows everything under budget, then over next month. Report only closed months and mark preliminary ones.
  • New GL accounts appear without a mapping and silently drop out of the totals; the unmapped check must be zero.
  • Phasing: an annual budget divided by 12 compares badly against seasonal spend. Phase the budget by month when it is set.
  • Editing the budget to make variances disappear destroys the baseline. Change the forecast instead.
  • Headcount timing is usually the largest variance driver; track planned vs actual start dates separately.

Verification

  • Total actuals on Actuals equal the accounting P&L total for each month to the cent.
  • Unmapped GL count is zero.
  • Variance signs follow the stated convention for both revenue and expense lines.
  • Full-year forecast = sum of closed actuals + open forecast months.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Budget Tracking"
  short_description: "Every variance explained, owned and forecast"
  default_prompt: "$budget-tracking Set up a budget vs actual tracker in Google Sheets from our approved budget and the Xero P&L export"
scripts/variance_report.py112 lines, 4.6 KB
#!/usr/bin/env python3
"""Budget vs actual variance report with materiality flags.

Input CSV columns (header required):
  line     budget line, e.g. "Marketing - Paid search"
  type     revenue | expense  (decides whether over-budget is favourable)
  month    YYYY-MM
  budget   budgeted amount for the month (blank = 0)
  actual   actual amount for the month (blank = not yet closed; month is skipped)

Usage:
  python3 variance_report.py budget_vs_actual.csv
  python3 variance_report.py budget_vs_actual.csv --month 2026-09 --pct 10 --abs 5000

A line is flagged when |variance| >= --abs AND |variance %| >= --pct, for the month
or year to date. Exit code 0 = no flags, 2 = flags found, 1 = bad input.
"""
import argparse
import csv
import sys
from collections import defaultdict
from decimal import Decimal, InvalidOperation


def num(value, where):
    value = (value or "").strip().replace(",", "")
    if value == "":
        return None
    try:
        return Decimal(value)
    except InvalidOperation:
        print(f"error: {where}: not a number: {value!r}", file=sys.stderr)
        sys.exit(1)


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("csv")
    ap.add_argument("--month", help="report month YYYY-MM (default: latest month with actuals)")
    ap.add_argument("--pct", type=Decimal, default=Decimal("10"), help="percent threshold (default 10)")
    ap.add_argument("--abs", type=Decimal, default=Decimal("1000"), help="absolute threshold (default 1000)")
    args = ap.parse_args()

    rows = defaultdict(dict)   # (line, type) -> month -> (budget, actual)
    closed = set()
    with open(args.csv, newline="", encoding="utf-8-sig") as fh:
        reader = csv.DictReader(fh)
        need = {"line", "type", "month", "budget", "actual"}
        if not reader.fieldnames or not need <= set(reader.fieldnames):
            print(f"error: CSV needs columns {sorted(need)}, got {reader.fieldnames}", file=sys.stderr)
            sys.exit(1)
        for n, r in enumerate(reader, start=2):
            kind = r["type"].strip().lower()
            if kind not in ("revenue", "expense"):
                print(f"error: line {n}: type must be revenue or expense, got {r['type']!r}", file=sys.stderr)
                sys.exit(1)
            b = num(r["budget"], f"line {n} budget") or Decimal(0)
            a = num(r["actual"], f"line {n} actual")
            month = r["month"].strip()[:7]
            rows[(r["line"].strip(), kind)][month] = (b, a)
            if a is not None:
                closed.add(month)
    if not closed:
        print("error: no actuals found", file=sys.stderr)
        sys.exit(1)
    month = args.month or max(closed)
    year = month[:4]

    def var(kind, b, a):
        v = a - b if kind == "revenue" else b - a      # positive = favourable
        p = (v / b * 100) if b else None
        return v, p

    def flag(v, p):
        return abs(v) >= args.abs and (p is None or abs(p) >= args.pct)

    hdr = f"{'line':<34}{'type':<9}{'budget':>12}{'actual':>12}{'var':>11}{'var%':>8}{'YTD bud':>13}{'YTD act':>13}{'YTD var':>11}{'YTD%':>8}  flag"
    print(f"Variance report for {month} (positive variance = favourable)\n")
    print(hdr)
    print("-" * len(hdr))
    flagged = 0
    totals = defaultdict(lambda: [Decimal(0)] * 4)
    for (line, kind), months in sorted(rows.items()):
        b, a = months.get(month, (Decimal(0), None))
        if a is None:
            a = Decimal(0)
        ytd = [(bb, aa) for m, (bb, aa) in months.items() if m[:4] == year and m <= month and aa is not None]
        yb = sum((x for x, _ in ytd), Decimal(0))
        ya = sum((y for _, y in ytd), Decimal(0))
        v, p = var(kind, b, a)
        yv, yp = var(kind, yb, ya)
        marks = []
        if flag(v, p):
            marks.append("MONTH")
        if flag(yv, yp):
            marks.append("YTD")
        flagged += bool(marks)
        t = totals[kind]
        t[0] += b; t[1] += a; t[2] += yb; t[3] += ya
        fp = lambda x: f"{x:7.1f}%" if x is not None else "    n/a"
        print(f"{line[:33]:<34}{kind:<9}{b:>12,.0f}{a:>12,.0f}{v:>11,.0f}{fp(p):>8}{yb:>13,.0f}{ya:>13,.0f}{yv:>11,.0f}{fp(yp):>8}  {' '.join(marks)}")
    print("-" * len(hdr))
    for kind, (b, a, yb, ya) in sorted(totals.items()):
        v, _ = var(kind, b, a)
        yv, _ = var(kind, yb, ya)
        print(f"{'TOTAL ' + kind:<43}{b:>12,.0f}{a:>12,.0f}{v:>11,.0f}{'':>8}{yb:>13,.0f}{ya:>13,.0f}{yv:>11,.0f}")
    print(f"\n{flagged} line(s) flagged at >= {args.pct}% and >= {args.abs:,} absolute")
    sys.exit(2 if flagged else 0)


if __name__ == "__main__":
    main()

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem