Schedule icon
OutputValues icon
If icon
Fail icon
Queries icon
Log icon
UploadFiles icon
Switch icon
Set icon

Reconcile the bank account and close the month, with an evidence pack

Automate month-end bank reconciliation in Kestra and DuckDB. Multi-pass matching, outstanding checks, journals, approval and a period lock.

Categories
BusinessData

The cash close is the month-end task every finance team does by hand, in a spreadsheet, under deadline. It is also mostly mechanical: match what can be matched, explain what cannot, book what the bank knows and the ledger does not, and prove the two balances agree. This flow does the mechanical part and leaves people only the decisions.

It matches the bank statement to the ledger in four passes, classifies every leftover line, and builds the reconciliation statement:

bank closing + deposits in transit + outstanding checks = book closing + proposed journals

It then proves the arithmetic: whatever difference is left must equal the sum of the items it could not explain. A close is allowed only when that sum is zero, and it is locked so the period cannot be closed twice or out of order.

It runs with no setup and no network. DuckDB generates a realistic month: 120 customer wires, daily card sales paid out net of fees with weekend batching, 60 vendor checks clearing one to two weeks late, two payroll runs, bank fees, interest and a returned item. The ISSUES scenario adds a duplicate posting, a transposed check amount and an unidentified deposit.

This blueprint was created by zkasuran.

Matching passes

Each pass only sees what earlier passes left, and a pair is kept only when each side is the other's best candidate, so no line is used twice.

Pass Rule Why
1 Same amount, same reference digits, dates within reference_window_days (45) A wire's invoice number or a check number is the strongest evidence. Checks clear late, so the window is wide
2 Same amount, dates within date_tolerance_days (5) Payroll and transfers without a usable reference. Equal amounts are common, so the window is short
3 One processor payout = several card-sales postings, net 95 to 99% of gross Card processors pay out net of fees and batch weekends. One bank line, three ledger lines
4 Same reference, amounts differ by a multiple of 9 The signature of swapped digits (1,245.30 vs 1,254.30). Flagged as a suspect, never matched automatically

How leftovers are classified

Class Side Meaning Effect
OUTSTANDING_CHECK book Written, not yet cleared Timing, deducted from the bank balance
DEPOSIT_IN_TRANSIT book Receipt in the last transit_window_days, or a card payout due next month Timing, added to the bank balance
BANK_FEE, INTEREST, RETURNED_ITEM bank The bank knows, the ledger does not yet Proposed journal
DUPLICATE_POSTING book Same reference posted twice Blocks the close
SUSPECTED_TRANSPOSITION both Pass 4 Blocks the close
UNIDENTIFIED_RECEIPT bank Money nobody can place Blocks the close
UNEXPLAINED_BOOK book A ledger line with no reason to be open Blocks the close

How it works

  1. period_control (io.kestra.plugin.core.output.OutputValues) reads the last closed period from the KV store and computes the previous period and the period end. period_gate (io.kestra.plugin.core.flow.If) refuses a period that is already closed, or one whose previous period is still open.
  2. reconcile (io.kestra.plugin.jdbc.duckdb.Queries) runs in one DuckDB session:
    • it builds gl and bank (the demo generator, or your data),
    • it runs the four passes into matches,
    • it classifies the rest into exceptions,
    • it proposes journals, debit and credit, including the processor fees found in pass 3,
    • it computes statement and returns one summary row with arithmetic_proven.
  3. log_reconciliation prints the passes, the timing items, both balances and the unexplained items. evidence (io.kestra.plugin.core.namespace.UploadFiles) stores matches.csv, exceptions.csv, proposed-journals.csv and statement.csv under close/<account>/<period>/ on every run.
  4. verdict decides between:
    • ARITHMETIC_ERROR: the difference does not equal the unexplained items, so a rule is wrong.
    • EXCEPTIONS: unexplained items or a non-zero difference.
    • NEEDS_APPROVAL: clean, but the journals exceed auto_post_limit and approved_by is empty.
    • READY.
  5. decide (io.kestra.plugin.core.flow.Switch):
    • READY: post_journals writes posted-journals.csv with the approver and execution id, and lock_period (io.kestra.plugin.core.kv.Set) records the close.
    • Every other verdict fails the run with what to do next. Nothing is posted or locked.
  6. second_business_day (io.kestra.plugin.core.trigger.Schedule), disabled until your sources are wired in.

Tested end to end

On Kestra 2.0.3 OSS.

Run Inputs Result
1 2026-09, CLEAN 190 bank and 212 ledger lines. Pass 1: 164, pass 2: 2, pass 3: 20 payouts covering 28 sales. 14 outstanding checks (-31,960.82), 4 deposits in transit (8,336.33). Bank and book both adjust to 614,106.79, difference 0.00. Journals total 10,209.48, so the run stops for approval
2 same, approved_by: Priya Nair (controller) Closed. posted-journals.csv carries the approver, KV cash_close_1010 holds the lock
3 2026-09 again Refused: already closed
4 2026-11 Refused: 2026-10 must be closed first
5 2026-10, ISSUES Blocked: difference -6,012.69, exactly the 4 unexplained items. INV-10037 posted twice (8,503.69), check 5012 booked -1,245.30 but paid -1,254.30 (diff -9.00), unidentified deposit 77812 (2,500.00)
6 2026-10, CLEAN, auto_post_limit: 20000 Closed without approval

The -6,012.69 row is the point of the arithmetic check: 2,500.00 + (-1,254.30) on the bank side, minus 8,503.69 + (-1,245.30) on the book side. Every cent of the difference is named.

Problems found while testing

  • Checks need a wider window than transfers. With one 5-day tolerance, 27 checks that cleared a week after issue were reported as unidentified, and the difference was -56,368.60. A reference match is strong evidence, so pass 1 now uses its own 45-day window. Pass 2, amount only, keeps the short one.
  • DuckDB refuses DATE + BIGINT. range() returns BIGINT, so day offsets are cast to INT.

Inputs

Input Type Default Purpose
period STRING 2026-09 YYYY-MM, validated
cash_account STRING 1010 The GL account, also the KV and folder key
scenario SELECT CLEAN Demo only: CLEAN or ISSUES
reference_window_days INT 45 Pass 1 window
date_tolerance_days INT 5 Pass 2 window
transit_window_days INT 3 Receipts this close to month end are in transit
auto_post_limit FLOAT 500.0 Journals above this need approved_by
approved_by STRING empty The reviewer, recorded on every posted line

Using it with your data

  • Replace the two generator statements with your exports, keeping the columns:
    • ledger: ref, memo, posted_on, amount, kind;
    • bank: bank_ref, description, value_date, amount. DuckDB reads CSV and Parquet directly. Bank formats such as camt.053 or MT940 need a small parser task first.
  • Replace the fixed opening balance with the statement's opening balance and the trial balance, and compare the statement opening with the previous period's closing balance stored in the KV lock.
  • Map the classification rules (SERVICE FEE, INTEREST, RETURNED ITEM) to your bank's descriptions, and the journal accounts (6810, 6820, 7100, 1200) to your chart of accounts.
  • Post the journals to your ERP from post_journals, for example with an HTTP task, instead of only writing the file.

Expected outputs

  • outputs.reconcile.outputs: the summary row, including difference, unexplained_items and arithmetic_proven.
  • Namespace files close/<account>/<period>/matches.csv, exceptions.csv, proposed-journals.csv, statement.csv and, after a close, posted-journals.csv.
  • KV cash_close_<account>: the last closed period, the adjusted balance, the journal total and the approver.

Things to know

  • Reopening a period is deliberate: delete the KV key. The flow never reopens one on its own.
  • Pass 4 flags any reference match whose amounts differ by a multiple of 9. Two unrelated differences can also be multiples of 9, which is why suspects block the close instead of being booked.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.