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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- budget-tracking Installs to ~/.agents/skills/budget-tracking/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install budget-tracking The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project budget-tracking Installs to .agents/skills/budget-tracking/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/budget-tracking.zipunzip -o budget-tracking.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) budget-tracking PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You are building the forward-looking model or runway forecast: use financial-modeling
- You need to audit a spreadsheet's formulas for errors: use spreadsheet-auditing
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.
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
- Read the approved budget (often from the financial model; see financial-modeling) and the chart of accounts. Confirm the fiscal year and currency.
- Map every GL account to exactly one budget line. Keep the map as its own tab.
- Import actuals for closed months from the accounting system export (QuickBooks, Xero, NetSuite, Sage; P&L by month as CSV).
- Compute monthly and YTD variance, flag material ones.
- Get explanations from budget owners; classify timing vs permanent.
- Update the full-year forecast (actuals for closed months + reforecast for the rest).
- Publish the review pack.
Tracker layout
| Tab | Content |
|---|---|
Budget | Approved budget, one row per line, months in columns; locked after approval |
GL_Map | GL account number, account name, budget line, owner |
Actuals_Raw | Pasted accounting export, unedited, with export date |
Actuals | SUMIFS of raw actuals by budget line and month via the map |
Variance | Month, YTD and full-year variance with flags |
Forecast | Closed months = actuals, open months = latest forecast |
Notes | Variance 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 size | Typical threshold (adjust to the company) |
|---|---|
| Small company, total budget under $2M | 10% and $2,500 |
| Mid-size department budgets | 10% and $10,000 |
| Board-level summary | 5% 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 type | Example | Forecast treatment |
|---|---|---|
| Timing | Annual conference invoice paid in September, budgeted in October | No full-year change |
| Accrual missing | Contractor invoice not received by close | Ask finance to accrue; no full-year change |
| Volume | More hires, more seats, higher usage | Change forecast for remaining months |
| Price | Vendor price increase, FX movement | Change forecast; consider renegotiation |
| One-off | Legal settlement, emergency repair | Forecast unchanged after this month; note separately |
| Misclassification | Cost booked to the wrong GL account | Reclass with finance; fix the map if needed |
| Budget error | Line budgeted with wrong assumption | Note 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
Actualsequal 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.yaml
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.py
#!/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.