Reconciliation Variance Explainer
Two exports in, the accounts that do not tie out, and a plain-English note on why they might not.
What it does
Loads two CSV exports — typically a GL trial balance and a subledger or bank export — matches them on account, and reports:
- every account where the two differ by at least your threshold, largest first
- every account present in one source but not the other, listed separately because that is usually a mapping problem rather than a real difference
Then the AI writes the covering note: whether the reconciliation is clean, and for each variance the most likely cause given the account (timing difference, unposted journal, cut-off, duplicate posting, FX) — stated as a hypothesis to check.
The message carries the narrative first and the full computed table underneath it.
Where the AI is, and is not
Every number is computed in plain python before the model is called. The narrate task receives the finished table as text. It never sees a source file, never adds anything up, and is instructed not to write a figure that is not already in front of it.
This matters more here than almost anywhere else in a firm: a model that does arithmetic on a trial balance will eventually produce a number that looks right and is not. Delete the narrate task entirely and the workflow still reconciles correctly — you lose the paragraph, not the reconciliation.
The causes it suggests are guesses from the account name. They are a starting point for the person doing the work, not a finding. The prompt says so, and the output says so.
What it does NOT do
It does not fetch the exports. Point it at files your accounting system already produces — a QuickBooks or Xero export, a bank CSV, a report your write-up software drops on a share. It does not post adjusting entries, and it does not decide what the correct balance is.
Setup
1. The two exports
Both need an account column and a balance column (Account / Balance also accepted). The loader handles what accounting exports actually contain:
- thousands separators —
12,450.00 - a leading currency symbol
- accounting negatives in parentheses —
(4,200.00)reads as-4200.00 - repeated accounts, which are summed — so a detail-level export reconciles against a summary one
A row whose balance will not parse is skipped rather than counted as zero.
2. Credential + contact
Replace the placeholders:
- <<GL_EXPORT_CSV>> — path to the GL export
- <<SUBLEDGER_EXPORT_CSV>> — path to the subledger or bank export
- <<VARIANCE_THRESHOLD>> — the absolute amount below which you do not care, e.g. 100
- <<ANTHROPIC_CREDENTIAL>> — an Anthropic credential (only the narrate task uses it)
- <<CONTROLLER_PUBLISHER>> — who gets the note
3. Schedule
After the exports land. Monthly for a close; daily for a cash account you want watched.
Customization
- Threshold.
<<VARIANCE_THRESHOLD>>. Set it to the number below which nobody would open the file. - Percentage instead of absolute. In
compose, replaceabs(diff) >= THRESHOLDwith a test againstabs(diff) / max(abs(a), 1)for a ratio. - Different column names. The
load()function names them in one place. - Local model. Swap the
anthropictask forollamaand no data leaves the building at all — the narrative is the only step that talks to a model, so this is a one-task change. - Drop the AI. Delete
narrateand set the message body to{{compose.stdout}}.
Source
workflow.iml
AI provider
Any supported AI provider works here. This template ships configured for the
anthropic tool; swap that task's tool to anthropic, openai, gemini, mistral,
ollama or openrouter to use the provider you already have, and point its
credential at your own key. The step is a plain summarise/classify call — nothing in
it is vendor specific. See requires.ai in the manifest.