Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
Developer Handover Pack
Bank Reconciliation Automation
[YourCompany.com] · Finance Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Bank Reconciliation automation. It covers the current-state process map with bottleneck identification, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. All build decisions, credential requirements, and implementation constraints are documented here. Nothing in this document requires action from the process owner; the FullSpec team owns everything described.
01Process snapshot
02Agent specifications
Connects to the bank via the Plaid API and to the accounting ledger via Xero, pulls all new transactions on schedule, and normalises both datasets into a unified format ready for matching. This agent fires on a set schedule (daily or month-end) or can be triggered manually for an out-of-cycle run. It replaces the manual statement export, cleaning, and ledger export steps that account for 50 minutes of bookkeeper time per cycle. The normalisation step must handle all account types and currencies currently in use before handing off to the Matching and Exceptions Agent.
// Input
plaid.transactions.get({
access_token: <PLAID_ACCESS_TOKEN>,
start_date: last_successful_run_date,
end_date: today,
account_ids: [<ACCOUNT_ID_1>, <ACCOUNT_ID_2>, ...]
})
xero.accounting.transactions.list({
DateFrom: last_successful_run_date,
DateTo: today,
Status: 'UNRECONCILED'
})
// Output
normalised_dataset: {
bank_transactions: [
{
txn_id: string,
account_id: string,
date: ISO8601,
amount: number, // positive = credit, negative = debit
currency: string, // ISO 4217
reference: string,
description: string,
merchant_name: string | null,
source: 'plaid'
}
],
ledger_entries: [
{
xero_txn_id: string,
contact_name: string,
date: ISO8601,
amount: number,
currency: string,
reference: string,
account_code: string,
status: 'UNRECONCILED',
source: 'xero'
}
],
run_metadata: {
run_id: uuid,
triggered_at: ISO8601,
trigger_type: 'scheduled' | 'manual',
bank_txn_count: number,
ledger_entry_count: number
}
}- Plaid requires the 'transactions' product enabled on the access token; confirm during Discovery that the connected institution supports Plaid's standard transactions endpoint and is not restricted to manual CSV fallback.
- Xero OAuth 2.0 scope must include 'accounting.transactions' and 'accounting.reports.read'. Confirm tenant ID is available and that the Xero subscription tier supports API access (Starter plans have monthly API call limits).
- The normalisation step must cast all amounts to a consistent signed-decimal format before handoff: credits as positive, debits as negative. Multi-currency accounts must carry the ISO 4217 currency code on every record.
- Dedupe logic: before passing records downstream, filter out any txn_id already present in the run log table to prevent double-processing on retry. Use the Plaid transaction_id as the idempotency key for bank records and the Xero BankTransactionID for ledger entries.
- If Plaid returns a PRODUCT_NOT_READY error (institution sync in progress), the agent must retry after a 5-minute delay up to three times before writing an error event to the log table and halting the run.
- Confirm with the process owner before build: how many bank accounts and currencies are in scope, and whether any accounts are held at institutions known to have limited Plaid coverage.
Applies rule-based and fuzzy matching logic to pair bank transactions with ledger entries from the normalised dataset delivered by the Bank Feed Sync Agent. Confirmed matches are posted back to Xero via the reconciliation API. Unresolved items (no match found or confidence below threshold) are written to the Google Sheets exceptions tab with pre-filled context including the bank reference, amount, date, and a suggested Xero account code based on description keywords. This agent replaces the four most time-intensive manual steps in the current process and is where calibration against historical data is most critical.
// Input
normalised_dataset: {
bank_transactions: [ { txn_id, date, amount, currency, reference, description } ],
ledger_entries: [ { xero_txn_id, date, amount, currency, reference, account_code } ],
run_metadata: { run_id, bank_txn_count, ledger_entry_count }
}
matching_config: {
exact_match_fields: ['amount', 'currency', 'reference'],
fuzzy_match_fields: ['description', 'merchant_name'],
fuzzy_threshold: 0.82, // tuned during QA against 3 months historical data
date_tolerance_days: 3,
confidence_post_threshold: 0.95 // below this -> exceptions sheet
}
// Output
matched_pairs: [
{
txn_id: string, // Plaid transaction_id
xero_txn_id: string, // Xero BankTransactionID
match_type: 'exact' | 'fuzzy',
confidence_score: number,
posted_to_xero: boolean,
xero_reconciliation_id: string
}
]
exceptions: [
{
txn_id: string | null,
xero_txn_id: string | null,
date: ISO8601,
amount: number,
currency: string,
bank_reference: string,
description: string,
suggested_account_code: string,
exception_reason: 'no_match' | 'low_confidence' | 'duplicate_candidate',
confidence_score: number | null
}
]
run_summary: {
run_id: string,
matched_count: number,
exception_count: number,
closing_balance: number,
currency: string,
completed_at: ISO8601
}- Fuzzy matching must use a normalised string-similarity algorithm (e.g. Jaro-Winkler or token-sort ratio) applied to the description and merchant_name fields after stripping punctuation and common noise words ('payment', 'ref', 'transfer').
- The fuzzy_threshold of 0.82 is a starting value. It must be calibrated by running the matching logic against three months of historical bank and ledger data in sandbox before any live posting occurs.
- Xero reconciliation API: use POST /BankTransactions/{BankTransactionID}/History to mark a transaction as reconciled. Confirm that the Xero account type supports this endpoint (BANK type only; CREDITCARD accounts use a different flow).
- The Google Sheets exceptions tab must use a predefined template with fixed column headers: txn_id, date, amount, currency, bank_reference, description, suggested_account_code, exception_reason, confidence_score, bookkeeper_action, resolved_at. Do not write to any other sheet tab.
- Dedupe: before writing to the exceptions sheet, check whether a row with the same txn_id already exists in the sheet. If it does, skip the write and log a warning. This prevents duplicate exception rows on retry.
- Confirm before build: the Xero tenant must have at least one bank account configured with a matching currency for every currency appearing in the Plaid feed. If a currency mismatch is found at runtime, the transaction must be routed to exceptions rather than silently dropped.
Fires after the Matching and Exceptions Agent completes its run and, separately, after the bookkeeper marks all exceptions as resolved in the Google Sheets tab. In the first phase it sends a Slack alert to the finance channel with the matched count, exception count, closing balance, and a direct link to the exceptions sheet. In the second phase, once the bookkeeper's sign-off is detected (a status cell in the sheet is set to 'APPROVED'), it posts the final reconciliation period close record to Xero and sends a confirmation email to the finance lead via Gmail. This agent replaces the manual summary preparation and distribution steps.
// Input (Phase 1 - post-match alert)
run_summary: {
run_id: string,
matched_count: number,
exception_count: number,
closing_balance: number,
currency: string,
exceptions_sheet_url: string,
completed_at: ISO8601
}
// Input (Phase 2 - close trigger)
sheet_event: {
run_id: string,
exceptions_status: 'APPROVED',
approved_by: string, // bookkeeper name from sheet cell
approved_at: ISO8601
}
// Output (Phase 1)
slack_message: {
channel: '#finance-reconciliation',
text: 'Reconciliation run {run_id} complete. Matched: {matched_count}. Exceptions: {exception_count}. Closing balance: {currency} {closing_balance}. Review exceptions: {exceptions_sheet_url}'
}
// Output (Phase 2 - on approval)
xero_period_close: {
BankAccountID: string,
StatementDate: ISO8601,
EndingBalance: number,
Notes: 'Reconciliation {run_id} closed by {approved_by} at {approved_at}. Matched: {matched_count}. Exceptions resolved: {exception_count}.'
}
gmail_confirmation: {
to: '<FINANCE_LEAD_EMAIL>',
subject: 'Reconciliation closed for period ending {StatementDate}',
body: 'The reconciliation run for {StatementDate} has been closed. {matched_count} transactions posted to Xero. All exceptions resolved and signed off by {approved_by}. Period close record written to Xero.'
}- Slack integration requires a bot token with the 'chat:write' scope installed to the workspace. Confirm the target channel name ('#finance-reconciliation' is a placeholder; confirm the actual channel with the finance lead before build).
- Gmail sending requires OAuth 2.0 with the 'gmail.send' scope authorised against the sending account. Do not use a personal Gmail account; use a shared finance team address or a dedicated service account.
- The Phase 2 trigger relies on a Google Sheets Apps Script webhook or a polling step that checks the exceptions_status cell every 5 minutes. Confirm which polling approach is supported by the chosen orchestration platform.
- Xero period close posting: use PUT /BankStatements to submit the closing statement. Confirm the BankAccountID for each account in scope before build. If Xero returns a validation error (e.g. overlapping statement period), the agent must halt and write the error to the log table rather than retrying silently.
- If exception_count is zero at Phase 1, the agent should skip the exceptions sheet link in the Slack message and proceed directly to posting the Xero close record without waiting for the Phase 2 approval trigger.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER
// ─────────────────────────────────────────────────────────────────
cron_schedule OR manual_webhook_call
-> orchestration_layer.start_run()
-> run_id = uuid.v4()
-> last_run_date = run_log_table.get_last_successful_date(run_id)
// ─────────────────────────────────────────────────────────────────
// AGENT 1: Bank Feed Sync Agent
// ─────────────────────────────────────────────────────────────────
plaid.transactions.get({
access_token: PLAID_ACCESS_TOKEN,
start_date: last_run_date,
end_date: today,
account_ids: [ACCOUNT_ID_1, ACCOUNT_ID_2]
})
-> raw_bank_transactions[] // fields: transaction_id, date, amount, iso_currency_code,
// merchant_name, name (description), account_id
xero.accounting.transactions.list({
DateFrom: last_run_date,
DateTo: today,
Status: 'UNRECONCILED'
})
-> raw_ledger_entries[] // fields: BankTransactionID, ContactName, DateString,
// Total, CurrencyCode, Reference, LineItems[].AccountCode
normalise(raw_bank_transactions, raw_ledger_entries)
-> normalised_dataset {
bank_transactions[]: { txn_id, account_id, date, amount, currency, reference, description, merchant_name }
ledger_entries[]: { xero_txn_id, contact_name, date, amount, currency, reference, account_code }
run_metadata: { run_id, triggered_at, trigger_type, bank_txn_count, ledger_entry_count }
}
// ─────────────────────────────────────────────────────────────────
// HANDOFF: Bank Feed Sync Agent -> Matching and Exceptions Agent
// ─────────────────────────────────────────────────────────────────
orchestration_layer.pass_payload(normalised_dataset)
// ─────────────────────────────────────────────────────────────────
// AGENT 2: Matching and Exceptions Agent
// ─────────────────────────────────────────────────────────────────
for each bank_transaction in normalised_dataset.bank_transactions:
exact_match = find_ledger_entry_where(
amount == bank_transaction.amount AND
currency == bank_transaction.currency AND
reference == bank_transaction.reference AND
abs(date_diff) <= 3 days
)
if exact_match:
matched_pairs.append({
txn_id, xero_txn_id, match_type: 'exact', confidence_score: 1.0
})
else:
fuzzy_score = jaro_winkler_similarity(
normalise_string(bank_transaction.description),
normalise_string(ledger_entry.reference)
)
if fuzzy_score >= 0.82 AND confidence_score >= 0.95:
matched_pairs.append({
txn_id, xero_txn_id, match_type: 'fuzzy', confidence_score: fuzzy_score
})
else:
exceptions.append({
txn_id, date, amount, currency, bank_reference: reference,
description, suggested_account_code,
exception_reason: 'no_match' | 'low_confidence',
confidence_score: fuzzy_score | null
})
// Post confirmed matches to Xero
for each pair in matched_pairs:
xero.PUT /BankTransactions/{pair.xero_txn_id}
-> IsReconciled: true
-> pair.xero_reconciliation_id = response.BankTransactionID
// Write exceptions to Google Sheets
google_sheets.append_rows(
spreadsheet_id: EXCEPTIONS_SHEET_ID,
sheet_tab: 'exceptions',
rows: exceptions[] // columns: txn_id, date, amount, currency, bank_reference,
// description, suggested_account_code,
// exception_reason, confidence_score,
// bookkeeper_action, resolved_at
)
run_summary = {
run_id, matched_count, exception_count,
closing_balance, currency,
exceptions_sheet_url, completed_at
}
// ─────────────────────────────────────────────────────────────────
// HANDOFF: Matching and Exceptions Agent -> Notifications and Close Agent (Phase 1)
// ─────────────────────────────────────────────────────────────────
orchestration_layer.pass_payload(run_summary)
// ─────────────────────────────────────────────────────────────────
// AGENT 3: Notifications and Close Agent (Phase 1 - alert)
// ─────────────────────────────────────────────────────────────────
slack.chat.postMessage({
channel: '#finance-reconciliation',
text: 'Run {run_id}: matched={matched_count}, exceptions={exception_count},
closing_balance={currency} {closing_balance}. Link: {exceptions_sheet_url}'
})
if exception_count == 0:
// Skip Phase 2 wait; proceed directly to Xero close
goto PHASE_2_CLOSE
// ─────────────────────────────────────────────────────────────────
// HUMAN STEP: Bookkeeper reviews exceptions in Google Sheets (~20 min)
// Sets exceptions_status cell to 'APPROVED' when complete
// ─────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────
// HANDOFF: Google Sheets approval event -> Notifications and Close Agent (Phase 2)
// ─────────────────────────────────────────────────────────────────
sheet_event: { run_id, exceptions_status: 'APPROVED', approved_by, approved_at }
-> orchestration_layer.trigger_phase_2(sheet_event)
// ─────────────────────────────────────────────────────────────────
// AGENT 3: Notifications and Close Agent (Phase 2 - close)
// ─────────────────────────────────────────────────────────────────
PHASE_2_CLOSE:
xero.PUT /BankStatements({
BankAccountID: BANK_ACCOUNT_ID,
StatementDate: today,
EndingBalance: closing_balance,
Notes: 'Reconciliation {run_id} closed by {approved_by} at {approved_at}'
})
-> xero_period_close_id
gmail.send({
to: FINANCE_LEAD_EMAIL,
subject: 'Reconciliation closed for period ending {StatementDate}',
body: '...matched={matched_count}, exceptions resolved, period close ID={xero_period_close_id}'
})
run_log_table.update({ run_id, status: 'COMPLETE', closed_at: ISO8601 })
// ─────────────────────────────────────────────────────────────────
// END OF FLOW
// ─────────────────────────────────────────────────────────────────04Recommended build stack
More documents for this process
Every document generated for Bank Reconciliation.