Back to Bank Reconciliation

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

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

Step
Name
Description
1
Export Bank Statement
Bookkeeper logs into the bank portal and downloads the statement as a CSV or PDF for the relevant period. (15 min)
2
Format and Clean Statement Data
Raw export is opened in a spreadsheet and reformatted so columns align with the accounting system layout. Duplicate and bank-fee rows are removed manually. (25 min)
3
Export Ledger Transactions
Bookkeeper runs a transaction report from Xero covering the same date range and exports it to a second spreadsheet tab. (10 min)
4 BOTTLENECK
Match Transactions Manually
Each bank line is compared against ledger entries by amount and date. Matching rows are marked as reconciled using a colour code or tick column. (60 min)
5 BOTTLENECK
Identify Unmatched Items
Any bank transaction without a ledger match, or vice versa, is flagged and pasted into a separate exceptions tab for investigation. (20 min)
6 BOTTLENECK
Investigate Discrepancies
Bookkeeper emails or messages relevant team members to confirm the nature of each unmatched transaction and waits for replies before updating the ledger. (30 min)
7
Post Adjusting Entries
Once discrepancies are resolved, the bookkeeper creates any necessary journal entries or coding corrections in Xero. (20 min)
8
Prepare Reconciliation Summary
A reconciliation summary showing opening balance, closing balance, matched items, and remaining exceptions is typed up and saved to a shared folder. (15 min)
9
Send Summary to Finance Lead
The completed reconciliation report is emailed to the finance manager for sign-off along with any open exception notes. (10 min)
10
File and Archive Records
The final spreadsheet and supporting documents are saved manually to the correct folder in the shared drive and the period is marked closed. (10 min)
Time cost summary: Total manual time per reconciliation cycle is 215 minutes (approximately 3 hours 35 minutes). At an assumed frequency of 4 to 5 runs per week the manual burden is approximately 5 hours per week and approximately 20 hours per 30-day period. The automation replaces steps 1, 2, 3, 4, 5, 7, 8, 9, and 10. Step 6 (exception investigation) is partially replaced: unmatched items are pre-populated with context automatically, but human review of genuine exceptions remains. The only remaining manual task is the bookkeeper's exceptions sign-off, estimated at 20 minutes per cycle.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Bank Feed Sync Agent

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.

Trigger
Cron schedule (configurable: daily at 06:00 local time and on manual webhook call for out-of-cycle runs)
Tools
Plaid (bank feed), Xero (ledger transaction pull)
Replaces steps
Steps 1, 2, and 3 (Export Bank Statement, Format and Clean Statement Data, Export Ledger Transactions)
Estimated build
10 hours, Moderate complexity
// 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.
Matching and Exceptions Agent

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.

Trigger
Fires immediately on receipt of the normalised_dataset payload from the Bank Feed Sync Agent
Tools
Xero (reconciliation posting), Google Sheets (exceptions log)
Replaces steps
Steps 4, 5, 7, and 8 (Match Transactions Manually, Identify Unmatched Items, Post Adjusting Entries, Prepare Reconciliation Summary)
Estimated build
12 hours, Complex complexity
// 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.
Notifications and Close Agent

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.

Trigger
Phase 1: on receipt of run_summary from Matching and Exceptions Agent. Phase 2: on Google Sheets webhook event where exceptions_status cell equals 'APPROVED'
Tools
Slack (finance channel alert), Gmail (confirmation email to finance lead), Xero (period close record posting)
Replaces steps
Steps 9 and 10 (Send Summary to Finance Lead, File and Archive Records)
Estimated build
6 hours, Simple complexity
// 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.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow for the Bank Reconciliation automation
// ─────────────────────────────────────────────────────────────────
// 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
// ─────────────────────────────────────────────────────────────────
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform with native support for scheduled triggers, webhook listeners, HTTP request nodes, and a shared credential store. One workflow per agent is the recommended structure: three discrete workflows corresponding to Bank Feed Sync Agent, Matching and Exceptions Agent, and Notifications and Close Agent. Shared credentials (Plaid access token, Xero OAuth token, Google Sheets service account, Slack bot token, Gmail OAuth token) are stored in the platform's centralised credential store and referenced by name in each workflow. No credentials are hardcoded in node configuration.
Webhook configuration
One inbound webhook endpoint is provisioned for the manual out-of-cycle trigger (Bank Feed Sync Agent). One inbound webhook or polling step is configured to receive or detect the Google Sheets 'APPROVED' status change (Notifications and Close Agent Phase 2). Both webhook URLs must be registered in the platform and documented in the credential store before QA begins. The Plaid and Xero integrations use outbound HTTP calls only; no inbound webhooks are required from those services for this build.
Templating approach
Slack and Gmail message bodies are constructed using inline string interpolation within the orchestration platform's expression editor. Template strings reference run_summary fields (run_id, matched_count, exception_count, closing_balance, currency, exceptions_sheet_url, StatementDate, approved_by, xero_period_close_id). No external templating engine is required. The Google Sheets exceptions tab uses a fixed column schema defined in the build spec; the platform appends rows using a structured append-row action, not a free-form write.
Error logging
A dedicated run_log table in Supabase records every workflow execution: columns are run_id (uuid), triggered_at (timestamptz), trigger_type (text), status (text: RUNNING, COMPLETE, FAILED, PARTIAL), bank_txn_count (int), ledger_entry_count (int), matched_count (int), exception_count (int), error_message (text, nullable), closed_at (timestamptz, nullable). On any node-level error the workflow catches the exception, writes a FAILED record with the error_message, and posts an alert to a dedicated '#automation-errors' Slack channel. The FullSpec team monitors this channel during the parallel-run QA phase. Supabase RLS policies restrict read access to authenticated service accounts only.
Testing approach
All three agents are built and tested against sandbox credentials first: Plaid sandbox environment (no real bank data), a Xero demo company tenant, a separate Google Sheets file used for QA, a test Slack channel ('#recon-test'), and a non-production Gmail address. Sandbox testing must validate exact matching, fuzzy matching at and below the 0.82 threshold, zero-exception runs, multi-currency records, and Plaid retry behaviour on PRODUCT_NOT_READY. After sandbox sign-off the build enters a two-cycle parallel run against live data, with outputs compared manually against the bookkeeper's results before the manual process is retired.
Estimated total build time
Bank Feed Sync Agent: 10 hours. Matching and Exceptions Agent: 12 hours. Notifications and Close Agent: 6 hours. End-to-end integration testing, QA parallel run, and edge-case resolution: 6 hours (included in the 28-hour total build effort). Total: 34 hours across the 4-week delivery window. Note: the template records 28 hours as the core build effort; the additional 6 hours covers end-to-end wiring, parallel-run monitoring, and handover documentation review.
Before any build node is created the following must be confirmed and recorded in the credential store: (1) Plaid access token and institution connectivity for every bank account in scope; (2) Xero OAuth 2.0 client ID, client secret, tenant ID, and API scope confirmation ('accounting.transactions', 'accounting.reports.read'); (3) Google Sheets spreadsheet ID and service account key with editor access to the exceptions sheet; (4) Slack bot token with 'chat:write' scope installed to the target workspace; (5) Gmail OAuth token with 'gmail.send' scope for the finance team sending address; (6) Supabase project URL and service-role key for the run_log table. Any missing credential delays the build start for that agent. Contact the process owner to resolve access blockers promptly. All questions go to support@gofullspec.com.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Bank Reconciliation.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View