AI Adoption GuideGovernmentReport
Variance explainer
LLM generates plain-language explanations of budget and output variances by pulling causal data from transaction records.
Government processPlanFundAuthorizeDeliverInspectEnforceReportClose
By Don, DoneThat’s AI coach · updated
Cite both the variance line and the transaction class
A usable variance note names two things: the line on the locked variance report, and the transaction class in the ledger that actually moved that line. If the ledger has no class that maps to the movement, the note stays empty. The model does not invent a cause. The budget analyst still writes the briefing.
That is the quality bar. A paragraph that restates the dollar gap is not an explanation. A paragraph that blames weather, seasonality, or "higher activity" without a coded class is a story. A story with no transaction class does not belong in the pack.
The report side of the cite is a specific row: fund, department, object, and the budget-to-actual or output-to-target cell the analyst already signed as the official variance. The ledger side is a class the chart of accounts already uses: overtime, professional services, grants receivable, capital outlay, a named transfer. Dual cite means both identifiers appear in the same sentence, in language a council analyst can check against the pack without opening a second narrative.
If those two identifiers cannot be joined, leave the cell blank. Empty is a control. Filled-in fiction is not.
Freeze the report pack and the ledger extract
Lock two artifacts before any sentence is generated. Snapshots move. An explanation written against yesterday's actuals and today's reclass will not survive the next export.
First, lock the variance report: the same file or view the department already uses for the period close, with line identifiers stable enough to quote. That pack may come out of a Microsoft finance workbook, an OpenGov budget book, a Tyler ERP extract, or an Anaplan-class planning model. The vendor does not matter. The freeze does. Record the report name, the period, and the run timestamp on the worksheet the model will read.
Second, lock the ledger extract for the same period and the same org units: posted transactions only, with transaction class (or object code, or expenditure category, whatever the jurisdiction actually codes). Exclude drafts, parked documents, and pending journals. If a reclass is still in workflow, it is not a cause yet.
Join keys must be boring: fund, department, object, project, and period. If the variance line is rolled up and the ledger is detailed, pre-aggregate the ledger to the report grain before the model sees either file.
Once both files are frozen, the task is mechanical. For each material variance line, retrieve the transaction classes that posted against that line in the period, rank them by contribution to the movement, and draft a sentence only when a class accounts for the movement in a way the join can prove. Lines that fail the join stay blank.
An anomaly-surfacing dashboard can flag which lines are worth explaining. It does not replace the freeze.
Dual cites, blank cells, and the weather-cause trap
Write every generated sentence in the same shape: variance line identifier, direction of the movement, transaction class, and the evidence grain (batch, object, or class total). Example shape: Parks overtime object, unfavorable to budget, posted overtime class OT-PARK for the period. The numbers live on the report. The sentence points at them.
Leave blanks on purpose. A line can be material and still have no usable class: interfund settlements that hit a clearing account, timing differences that reverse next period with no class change, output variances that never touch the general ledger. For output lines, the register the jurisdiction treats as official is the source: work-order class, permit type, caseload code. If that register is silent, the cell stays empty the same way a dollar line does.
The weather-cause trap is the most common fill-in. Snow, heat, tourism, a busy month: those may be true in the world and still be uncoded in the books. If storm overtime was posted to a disaster object, cite that object. If overtime hit the same object as every other week, you do not get to add weather. The model must not reach for a cause outside the extract. Absence of a class is a stop, not a prompt to be helpful.
A story with no transaction class is the same failure in a longer coat. "Professional services ran high because the vendor invoices arrived late" is a story unless the extract shows a professional-services class (and, if the jurisdiction codes it, a late-post flag) on that line. Late invoices that posted to the right class are a timing note the analyst can add in the briefing. They are not a substitute for the class cite in the generated cell.
Treat the generated column as a draft only. The failure mode is treating the draft as the briefing: pasting the column into the transmittal, the council memo, or the public-facing data summarizer without an analyst pass. Generated prose is a pointer. It is not a finding, a recommendation, or a public sentence.
Parks overtime: one line, one payroll class
The locked variance report shows Parks, personal services, overtime object, unfavorable to budget for the period. The locked ledger extract for the same fund, department, object, and period contains posted payroll batches coded to overtime class OT-PARK, plus a small amount of regular pay that should not be in that object. The join at report grain attributes the movement to OT-PARK. Regular pay in the overtime object is a coding exception. It does not become the explanation unless the analyst separately decides the miscode is the story.
The generated cell cites both: the Parks overtime variance line (report row id) and OT-PARK (transaction class). It does not mention a heat wave. It does not mention festival season. It does not guess how much of the gap is operational versus one-time. Those sentences belong in the briefing, if the analyst has evidence beyond the extract.
If OT-PARK is missing from the extract, or the overtime object on the report does not join to any payroll class, the cell stays blank. The analyst still briefs. They may say the books do not support a class-level cause this period, and they may take a question. They do not fill the blank with weather.
Output variance on the same department follows the same rule. If Parks also reports acres mowed below target, the operational register must carry a work class that moved acres. No class, no sentence. Do not borrow the overtime dollar explanation to cover an output miss. Dollars and outputs are different lines.
This is also where a resource allocation optimizer can mislead if someone feeds it the draft notes as if they were causes. Allocation logic needs coded drivers. A blank cell is information: do not reallocate on a guessed narrative.
What finance systems already hold
Microsoft, OpenGov, Tyler, and Anaplan-class planning tools already store the ingredients: adopted and revised budget, actuals, sometimes output targets, object codes, and exports an analyst can freeze. None of them, as a class, write a dual-cited variance sentence that is allowed to be blank. That is the gap this workflow fills. Do not rank the vendors. Do not assume a module exists. Pull the same two extracts you already send to close.
Planning models are easy to misuse as a cause library. A driver in an Anaplan-class model is a planning assumption, not a posted class. If the model says overtime rises with event-days, that is not a ledger cite unless event-days and overtime class both posted.
When the variance pack feeds a legislative compliance report assembler, keep the generated column out of statutory exhibits until the analyst has accepted or blanked each row. Assemblers copy what they are given. A weather sentence in a required variance narrative is still a weather sentence.
The analyst still briefs
The model drafts. The analyst briefs. Briefing means: accept a dual-cited cell, blank a cell the join cannot support, and add judgment the extract cannot hold. Judgment sits in the briefing memo, attributed to the analyst. It does not get written back into the generated column as if the ledger had said it.
Do not read the generated column aloud in committee as the department's explanation. A blank cell is the honest output when the ledger is silent. Quality is a pair of cites or an empty cell, every time.
Is this worth automating for you?
Whether this pays back depends on how much time it takes your team today. Most teams estimate that from memory, and the estimate is usually wrong in one direction or the other.
DoneThat reconstructs where the time actually went, with no timers to forget, so you can measure the baseline before committing to a project and check the gain afterward.
Measure the baseline first