Calculate the variances yourself, then give the model the table and a strict prompt. It may use only figures in the table and causes in your evidence notes, and any line without evidence becomes a flagged question. Check every number in the draft against its own line before anyone reads it.
Why Does AI Commentary Make Up Causes?
A language model is built to produce a fluent answer. Ask why hosting rose and give it no reason, and it will often supply a plausible one. In a board pack, a plausible wrong cause is worse than a blank.
Popular finance prompts make this easy to trigger. One FP&A Trends prompt asks the model to judge the Q4 pipeline impact of two delayed deals, with no pipeline data supplied. Another asks for "context on ROI" and supplies no ROI figures.
Neither sets a threshold or asks the model to check that components add up to the total (FP&A Trends). Other guides tell you to "always verify outputs" but give no method (Learnsignal).
Anthropic's docs point to the fix. Among other techniques, they suggest letting the model say it doesn't know, restricting it to the documents provided, and requiring citations. The same page warns that these techniques reduce hallucinations but "don't eliminate them entirely" (Anthropic).
That's why step 4 exists.
Step 1: Calculate the Variance Table First
Never ask the model to subtract. Build the table in your spreadsheet or system and apply your threshold. Before drafting, tie the budget and actual columns to the approved budget and the trial balance.
Sample table. All figures are sample numbers for illustration, not client data.
Sample Co., March 2026, budget vs actual (US$). Variance = actual minus budget. Threshold: US$10,000, or 10% where the line moved at least US$2,500.
| Line | Budget | Actual | Variance | Var % | F/U | Over threshold? | Evidence notes |
|---|---|---|---|---|---|---|---|
| Subscription revenue | 1,240,000 | 1,182,000 | -58,000 | -4.7% | U | Yes | Ending customers 1,050 vs 1,100 budget; 31 churned vs 18 budget |
| Services revenue | 160,000 | 191,500 | +31,500 | +19.7% | F | Yes | Two implementation projects finished in March, budgeted for April |
| Hosting (COGS) | 148,000 | 171,300 | +23,300 | +15.7% | U | Yes | Vendor usage report: compute hours 18% above budget |
| Paid media | 95,000 | 88,400 | -6,600 | -6.9% | F | No | |
| G&A payroll | 210,000 | 212,100 | +2,100 | +1.0% | U | No | |
| Software subscriptions | 32,000 | 41,600 | +9,600 | +30.0% | U | Yes | None supplied |
Four lines cross the threshold and three have evidence. The fourth has none on purpose, to test whether the prompt holds.
Step 2: Use a Prompt That Fences the Model In
Copy this template. For the sample, the evidence notes are the table's last column.
ROLE
You draft month-end variance commentary for a finance manager to review.
INPUTS
1. VARIANCE TABLE: the only numbers you may use.
2. EVIDENCE NOTES: the only causes you may cite.
3. Comment only on lines where "Over threshold?" = Yes.
RULES
- Use only numbers that appear in the VARIANCE TABLE or EVIDENCE NOTES.
Copy them exactly. Do not calculate, round or combine numbers.
- Cite a cause only if it appears in the EVIDENCE NOTES for that line.
After each cause, quote the note in square brackets.
- If a line has no evidence, do not guess. Write:
"OPEN QUESTION: [line] is [variance] ([var %]) vs budget.
No cause in the data supplied." Then suggest who might know.
- Do not use general knowledge about the industry, economy or seasonality.
- Use the F/U column for favorable or unfavorable.
- Plain business English. No judgment words like "strong" or "concerning".
FORMAT
1. One bullet per line over threshold:
[Line]: [variance] ([var %]) [F/U] vs budget. [Cause.] [Evidence: "..."]
2. A section titled OPEN QUESTIONS.
3. A section titled NUMBERS USED listing every number you wrote.
VARIANCE TABLE
<paste table here>
EVIDENCE NOTES
<paste notes here, one per line, labeled by line name>
Step 3: Read the Draft Against the Fences
Sample output generated with Claude (Anthropic), October 2026, from the prompt above. Review every line before use.
- Subscription revenue: -58,000 (-4.7%) U vs budget. Ending customers were 1,050 vs 1,100 budget, and 31 customers churned vs 18 budget. [Evidence: "Ending customers 1,050 vs 1,100 budget; 31 churned vs 18 budget"]
- Services revenue: +31,500 (+19.7%) F vs budget. Two implementation projects finished in March that were budgeted for April. [Evidence: "Two implementation projects finished in March, budgeted for April"]
- Hosting (COGS): +23,300 (+15.7%) U vs budget. Compute hours were 18% above budget. [Evidence: "Vendor usage report: compute hours 18% above budget"]
- Software subscriptions: +9,600 (+30.0%) U vs budget. No cause in the data supplied. See OPEN QUESTIONS.
OPEN QUESTIONS
- OPEN QUESTION: Software subscriptions is +9,600 (+30.0%) vs budget. No cause in the data supplied. The budget owner for software subscriptions, or the AP team, may be able to explain it.
NUMBERS USED
-58,000; -4.7%; 1,050; 1,100; 31; 18; +31,500; +19.7%; +23,300; +15.7%; 18%; +9,600; +30.0%
Notice what compliant output leaves out. It doesn't say services will miss in April, even though that's likely. That forward view is the reviewer's call.
A good reviewer also asks why hosting cost ran 15.7% over budget when hours ran 18% over. A committed-use discount, or a rate change? That question is the reviewer's job, not the model's.
Step 4: Check Every Number Against Its Own Line
A number that exists somewhere in the table isn't enough. The real failure is a correct figure on the wrong line, so the check matches each sentence to the line it names.
- Read each row of the table, evidence included, into a set of numbers.
- For each line of the draft, find the row name it mentions.
- Flag any number that isn't in that row. Under NUMBERS USED, check against the whole table.
- Reject the draft if anything is flagged, and fix the prompt or the inputs.
We ran this on the sample, and it passes the output above. It fails a draft with services' figures (+31,500 and +19.7%) moved onto the hosting line. It also fails one with an invented "4,000 extra hours."
import re
NUM = r"[-+]?\d+(?:,\d{3})*(?:\.\d+)?%?"
def rows_from(table_md):
"""Line name -> numbers in that row, evidence notes included."""
rows = {}
for row in table_md.strip().splitlines()[2:]: # skip header and |---|
cells = [c.strip() for c in row.strip().strip("|").split("|")]
rows[cells[0]] = set(re.findall(NUM, " ".join(cells[1:])))
return rows
def check(draft, table_md):
rows = rows_from(table_md)
every = set().union(*rows.values())
problems, in_list = [], False
for text in draft.splitlines():
in_list = in_list or text.strip().startswith("NUMBERS USED")
named = sorted((text.find(n), n) for n in rows if n in text)
if in_list:
allowed, where = every, "source"
elif named:
allowed, where = rows[named[0][1]], named[0][1]
else:
allowed, where = set(), "a named line"
problems += [f"{n} not in {where}" for n in re.findall(NUM, text)
if n not in allowed]
return problems
# table_md: the markdown table above. draft: the model's output.
problems = check(draft, table_md)
print("Reject draft:" if problems else "Numbers pass.", problems or "")
The check is strict about signs, so "23,300" fails where the table says "+23,300." It doesn't read numbers written in words ("twenty-three thousand"), and it can't tell a right cause from a wrong one. A person still reads every line.
What to Automate vs Keep With a Person
Automate (draft or calculate):
- Variance math, threshold flags and the tie-out to the trial balance
- First-draft sentences for lines with evidence
- The open-question list for lines without evidence
- The line-by-line number check
Keep with a person:
- Chasing evidence notes from budget owners
- Judging whether a cited cause is the real cause
- Forward-looking statements, and what the board should watch
- Tone, emphasis, what to leave out and the final sign-off
Controls and Audit Trail
For each close, keep the variance table, evidence notes, prompt version, raw draft, check result and the reviewer's edited version. That set lets your auditor, board or a buyer's diligence team trace each sentence to a number and a source. Change the prompt between closes, never during one.
For where commentary fits in the wider close, see Can AI Do the Month-End Close?
FAQ
Can I paste my trial balance into ChatGPT and ask for commentary? Only on an approved business plan (see our checklist). Even then, a bare trial balance invites invented causes. Calculate variances first, add evidence notes, and use a fenced prompt like the one above.
What threshold should I use for commentary? Use the one your board or lender already expects. A common rule is an amount or a percentage, whichever is hit first. Add a dollar floor so tiny accounts with big percentage swings don't flood the list.
Why not let the model calculate the variances? Language models can make arithmetic errors. A spreadsheet gives the same answer every time, and you can tie its totals to the ledger. A model's arithmetic can't be tied out the same way.
What if most lines come back as open questions? That tells you the evidence is thin, not that the prompt failed. Collect better notes from budget owners before the next close.
Flux is your embedded finance team. We run one workflow free on a period you've already closed, so you can compare it line by line.
Contact us