Table of Contents
- 1 The Business Situation
- 2 The Existing Process
- 2.1 Process weaknesses
- 2.2 Business effects
- 3 What the New System Needed to Do
- 4 Implementation Approaches Considered
- 4.1 Expanded Excel formulas
- 4.2 Excel Power Query with Power Automate
- 4.3 Google Sheets and Apps Script
- 4.4 Custom application
- 4.5 Dedicated reconciliation software
- 5 The Selected Solution
- 6 System Architecture and Data Flow
- 7 Data Structure
- 7.1 Reconciliation run record
- 7.2 Transaction import and review records
- 8 Workflow Statuses and Ownership
- 9 Step-by-Step Implementation
- 9.1 Step 1: Prepare the Accounts and Permissions
- 9.2 Step 2: Build the Intake
- 9.3 Step 3: Create the System of Record
- 9.4 Step 4: Connect the Tools
- 9.5 Step 5: Build the Core Automation
- 9.6 Step 6: Add Approvals, Reminders, and Escalations
- 9.7 Step 7: Add Documents and File Management
- 9.8 Step 8: Add Reporting and Operational Views
- 9.9 Step 9: Add Security and Governance Controls
- 9.10 Step 10: Deploy and Test
- 10 Code and Configuration
- 10.1 Power Query matching query
- 10.2 Power Query summary query
- 10.3 Power Automate approval configuration
- 11 Failure Handling and Operational Reliability
- 12 A Complete Example
- 13 Implementation Cost
- 14 Estimated Time and Cost Savings
- 15 Adding AI to the Automation
- 15.1 The Recommended AI Enhancement
- 15.2 Benefits of the AI Enhancement
- 15.3 What Remains Rule-Based or Human-Controlled
- 15.4 Estimating the Additional Value of AI
- 16 Testing Checklist
- 17 Ongoing Maintenance
- 18 When to Move to Dedicated Software
- 19 Implementation Checklist
The Business Situation
Harborstone Equipment Services is a fictional 58-person equipment maintenance and rental business. Its four-person finance department uses QuickBooks Online as its accounting system, online banking portals for three cash accounts, Excel for reconciliations, and Microsoft 365 for document storage and communication.
The monthly reconciliation process involves approximately 1,250 bank transactions and 1,180 accounting transactions across an operating account, payroll account, and merchant settlement account. The senior accountant prepares the reconciliations, the controller reviews them, and the finance director acts as the backup approver for high-risk exceptions or controller absences.
Each month, the senior accountant exports transaction data from the bank portals and QuickBooks Online. The files are copied into spreadsheets, reformatted, and compared using filters, formulas, sorting, and manual review. The method works at low volume, but it is difficult to reproduce consistently and depends heavily on the preparer remembering each step.
The business wanted to retain QuickBooks Online and Excel while introducing a controlled process for standardized imports, exact matching, reference-based matching, cautious fuzzy suggestions, exception review, sign-off, and evidence retention.
Note: This case study is provided as a representative example of the types of AI integration and digital transformation solutions Intelligex designs and delivers. Actual engagements are tailored to each client’s goals, constraints, existing systems, timeline, and available resources, so the approach, tools, and outcomes may vary.
The Existing Process
The original reconciliation followed the same general sequence each month, but many details were undocumented.
- The senior accountant downloaded bank activity from three bank portals.
- A transaction-detail report was exported from QuickBooks Online for each corresponding bank account.
- The exports were copied into separate workbook tabs.
- Dates, amounts, references, and descriptions were reformatted manually.
- Transactions with identical amounts were filtered and compared visually.
- Lookup formulas were added to find exact amounts or references.
- Potential matches were highlighted using cell colors.
- Unmatched transactions were copied to an exception tab.
- Questions were sent through email to accounts payable, payroll, or operations.
- The completed workbook was emailed to the controller for review.
- Reviewer comments were returned by email, and the workbook was updated again.
- A final copy was saved in a monthly finance folder.
Process weaknesses
- Bank and ledger files used different headings and date formats.
- Amount signs were not always applied consistently.
- Lookup formulas could select the wrong transaction when amounts repeated.
- References containing spaces, hyphens, or prefixes were difficult to compare.
- Manual highlights did not provide durable match evidence.
- Exception ownership was tracked in email rather than in the reconciliation.
Business effects
- Preparers spent time reformatting data before reviewing it.
- Duplicate amounts increased the risk of false matches.
- The controller could not easily distinguish rules-based matches from manual decisions.
- Reviews were delayed when the preparer was unavailable.
- Reporting on recurring exceptions required additional spreadsheet work.
- Approval evidence was distributed across files, email, and flow history.
The largest control issue was not that Excel was being used. The issue was that the workbook had no stable data model, deterministic matching order, controlled exception process, or reliable approval record.
What the New System Needed to Do
The finance team defined the requirements before selecting an automation method.
| Requirement | Required behavior |
|---|---|
| Standardized intake | Convert bank and QuickBooks exports into one canonical set of fields and amount conventions. |
| Run identification | Assign a unique reconciliation ID to each account and statement period. |
| Exact matching | Match only unique account, date, and amount combinations. |
| Reference matching | Match unique account, amount, and normalized reference combinations within a controlled date tolerance. |
| Fuzzy comparison | Suggest description-based candidates only after exact and reference matching, without automatically approving them. |
| Duplicate prevention | Prevent one bank transaction or ledger transaction from being accepted more than once. |
| Exception review | Keep unresolved bank and ledger transactions in a visible queue with an owner and decision. |
| Balance control | Compare adjusted bank and QuickBooks ending balances in cents. |
| Human control | Require a finance employee to confirm fuzzy suggestions, timing differences, bank errors, and unusual items. |
| Approval | Route completed reconciliations to the designated approver and record the response. |
| Evidence retention | Preserve source exports, the final workbook, approval details, and supporting documents. |
| Reliability | Record failed flow runs, invalid data, duplicate identifiers, and blocked approvals. |
| Reporting | Show unmatched items, aging, match methods, owners, exception categories, and automation status. |
| Manual override | Allow documented exceptions without allowing users to overwrite calculated query results. |
The design deliberately separated automated matching from accounting judgment. Exact and unique reference matches could be classified automatically. Fuzzy matches, timing classifications, bank errors, and approval remained human-controlled.
Implementation Approaches Considered
| Approach | Connected tools | Effort | Control level | Main limitation |
|---|---|---|---|---|
| Expanded Excel formulas | QuickBooks exports and Excel | Low | Moderate | Formula logic, manual decisions, and copied tabs become difficult to govern. |
| Excel Power Query with Power Automate | QuickBooks Online, Excel, SharePoint, Power Query, Power Automate | Moderate | Strong for the stated volume | Desktop refresh and controlled human review remain necessary. |
| Google Sheets and Apps Script | Export files, Google Sheets, Apps Script, Google Drive | Moderate | Customizable | Requires moving a Microsoft-centered finance process into a second productivity suite. |
| Custom application and database | QuickBooks API, bank files or APIs, SQL database, custom interface | High | High | More development, security, support, and monitoring than the volume justified. |
| Dedicated reconciliation software | Accounting platform, bank feeds, reconciliation platform | Moderate to high | Potentially very high | Additional licensing, implementation, and process change may exceed the immediate need. |
Expanded Excel formulas
Formulas such as XLOOKUP, COUNTIFS, and helper columns could improve the existing workbook. This option was inexpensive and familiar, but it did not adequately address transformation logic, repeated imports, matching order, approval routing, or evidence retention. Formula-based matches were also vulnerable to duplicate amounts unless substantial control logic was added.
Excel Power Query with Power Automate
Power Query could standardize both exports, calculate stable matching keys, enforce matching order, and produce repeatable output tables. Power Automate could manage run creation, approval routing, reminders, status updates, and evidence files. This approach retained the company’s existing tools and was appropriate for approximately 1,250 bank transactions per month.
Google Sheets and Apps Script
Apps Script could parse files, apply matching logic, and create approval notifications. It was not selected because the finance team already used Excel, SharePoint, and Microsoft identities. Introducing Google Drive would create another permission model and document repository without solving a specific requirement that Power Query and Power Automate could not meet.
Custom application
A custom application could support direct APIs, a database, granular permissions, and a purpose-built review interface. It was not justified for the current volume because the business would assume responsibility for application hosting, authentication, monitoring, backups, and ongoing development.
Dedicated reconciliation software
Dedicated software should be evaluated when transaction volume, entity count, regulatory requirements, or intercompany complexity increases. For Harborstone Equipment Services, the immediate requirement was a controlled monthly process rather than a broad financial-close platform.
The Selected Solution
The selected implementation used QuickBooks Online as the accounting source, Excel as the reconciliation interface, Power Query as the transformation and matching engine, SharePoint as the run register and evidence repository, and Power Automate as the workflow and approval layer.
| Tool | Responsibility |
|---|---|
| QuickBooks Online | Accounting system and source of bank-account ledger transactions. |
| Bank portals | Source of statement-period bank transactions and ending balances. |
| Excel | Controlled import tables, review decisions, query output, and reconciliation summary. |
| Power Query | Data typing, normalization, deterministic matching, fuzzy candidate generation, exception output, and control totals. |
| SharePoint document library | Template storage, source exports, working workbooks, support, approved evidence, and version history. |
| SharePoint list | Run-level system of record for owner, status, dates, approvers, file links, and approval evidence. |
| Power Automate | Run setup, validation, approval routing, reminders, notifications, status updates, and evidence-file creation. |
| Excel PivotTables or Power BI, optional | Operational reporting from approved reconciliation summaries and exception history. |
| Approved AI service, optional | Suggested categorization and summarization of unresolved exceptions after the core process is stable. |
QuickBooks Online remained the accounting authority. The implementation did not create journal entries, release payments, or alter accounting records. If a bank-only transaction required a QuickBooks entry, finance posted it in QuickBooks Online, exported refreshed ledger data, and reran the query.
The desktop version of Excel was retained because it provides the required Power Query authoring and refresh capabilities. Power Automate did not attempt to refresh the workbook’s Power Query connections. The preparer refreshed and validated the workbook before changing the run status to Ready for Approval.
This boundary avoided an unreliable assumption that an Excel Online action would refresh every desktop Power Query connection. Power Automate read the already-refreshed summary table and blocked approval if the workbook reported unresolved items or a nonzero control difference.
System Architecture and Data Flow
- Intake: Bank exports, QuickBooks Online exports, statement balances, and supporting documents uploaded to a SharePoint run folder.
- System of record: A SharePoint list stores one item per reconciliation run; the workbook stores transaction-level decisions and calculated results.
- Automation layer: Power Query performs data transformation and matching; Power Automate manages run workflow and approval.
- Document storage: SharePoint stores source files, the working workbook, approved copies, and approval evidence.
- Notifications: Power Automate sends approval, reminder, rejection, completion, and failure notifications through Outlook or Teams.
- Reporting: Excel output tables and optional Power BI reports summarize volume, match rates, exceptions, aging, and completion.
- AI layer: An optional approved language model proposes exception categories and summaries but does not approve matches or accounting treatment.
- Create the run. The senior accountant creates a SharePoint list item with account, statement period, preparer, approver, and ending-balance information. Power Automate assigns the reconciliation ID, creates the folder structure, copies the workbook template, and returns the workbook link.
- Collect source evidence. The preparer exports bank transactions and the corresponding QuickBooks Online account activity. Original files are uploaded without modification to the run’s
Sourcefolder. - Load canonical tables. Required columns are pasted or mapped into the workbook’s controlled bank and ledger import tables. Each row retains its source file and source row number.
- Validate inputs. Power Query checks required fields, data types, reconciliation IDs, transaction identifiers, amount signs, and duplicate row IDs. Invalid input stops the refresh rather than producing partial matching results.
- Apply exact matching. The query joins account, date, and amount. A match is accepted only when the combination occurs once on each side.
- Apply reference matching. Remaining transactions are joined using account, signed amount, and normalized reference. Candidates must fall within the configured date tolerance and remain one-to-one.
- Generate fuzzy suggestions. Remaining bank rows are compared with remaining ledger rows having the same account and exact amount within a narrow date window. Description similarity creates suggestions, not final matches.
- Review exceptions. The preparer records decisions in a separate input table. The query validates that an accepted fuzzy pair is still eligible and that the ledger row is not used elsewhere.
- Calculate controls. Power Query calculates matched counts, unresolved counts, timing differences, bank-error adjustments, adjusted bank balance, and the difference from the QuickBooks ending balance.
- Request approval. The preparer refreshes the workbook, confirms the output, closes the file, and changes the SharePoint item to
Ready for Approval. - Validate readiness. Power Automate reads the workbook summary table. If unresolved items remain or the difference is not zero cents, the run is changed to
Blockedand returned to the preparer. - Route sign-off. If validation passes, Power Automate starts the controller approval. A secondary approval is added when policy-defined high-risk conditions apply.
- Retain evidence. On approval, Power Automate updates the SharePoint run, copies the workbook to the approved folder, and writes a structured approval-evidence file.
- Handle failure. A failed flow writes an error status and message to the run item, notifies the support owner, and leaves the source files and workbook available for controlled recovery.
Data Structure
The design uses one reconciliation run per bank account and statement period. Three accounts therefore produce three monthly run IDs, even when the finance team reviews them together.
Reconciliation run record
| Field | Type | Required | Allowed value or example | Purpose |
|---|---|---|---|---|
| ReconID | Single-line text, unique | Yes | REC-2026-05-OPERATING | Primary business identifier used in folders, files, tables, and evidence. |
| AccountKey | Choice or controlled text | Yes | OPERATING, PAYROLL, MERCHANT | Identifies the account without storing a full bank account number. |
| StatementStartDate | Date | Yes | 2026-05-01 | Beginning of the statement period. |
| StatementEndDate | Date | Yes | 2026-05-31 | End of the statement period. |
| BankEndingBalance | Currency | Yes | 184532.47 | Statement ending balance entered from bank evidence. |
| QBOEndingBalance | Currency | Yes | 182690.07 | QuickBooks register or report ending balance for the same cutoff. |
| Preparer | Person | Yes | Senior accountant | Owns import, review, and correction. |
| Approver | Person | Yes | Controller | Primary sign-off owner. |
| Status | Choice | Yes | Draft, Awaiting Files, In Review, Ready for Approval, Approval In Progress, Approved, Rejected, Blocked, Automation Failed | Controls workflow routing. |
| WorkbookIdentifier | Text | Yes after creation | SharePoint file identifier | Allows Power Automate to read the correct Excel table. |
| WorkbookLink | Hyperlink | Yes after creation | Run workbook URL | Gives users and approvers direct access. |
| ApprovalRequestID | Text | No | Returned approval identifier | Prevents duplicate approval creation and supports audit tracing. |
| ApprovalOutcome | Choice | No | Approved, Rejected | Stores final response. |
| ApprovalComments | Multiple-line text | No | Reviewer explanation | Retains reviewer comments outside transient notifications. |
| ApprovalCompletedAt | Date and time | No | UTC timestamp | Records completion time. |
| AutomationStatus | Choice | Yes | Pending, Running, Succeeded, Failed, Manual Recovery | Separates automation health from business workflow status. |
| LastAutomationRun | Date and time | No | UTC timestamp | Supports monitoring. |
| RetryCount | Number | Yes | 0 to 3 | Records automated or controlled retries. |
| ErrorMessage | Multiple-line text | No | Sanitized failure description | Supports recovery without exposing credentials. |
Transaction import and review records
| Field | Type | Required | Source | Validation and purpose |
|---|---|---|---|---|
| ReconID | Text | Yes | Run control | Must equal the workbook parameter. |
| AccountKey | Text | Yes | Export mapping | Must equal the run account. |
| TransactionDate | Date | Yes | Bank or QuickBooks export | Must be a valid date within the configured import window. |
| Amount | Decimal currency | Yes | Bank or ledger export | Deposits are positive and withdrawals are negative. |
| AmountCents | 64-bit integer | Calculated | Power Query | Prevents floating-point equality problems during matching. |
| Description | Text | Yes | Source export | Retained as original evidence and normalized for candidate comparison. |
| Reference | Text | No | Check number, payment reference, deposit ID, or transaction reference | Spaces and punctuation are removed for reference matching. |
| BankTransactionID | Text | Preferred | Bank export | Used as the stable bank row identifier when available. |
| QBOTransactionID | Text | Preferred | QuickBooks export or controlled source reference | Used as the stable ledger row identifier when available. |
| SourceFileName | Text | Yes | Preparer or file adapter | Links the normalized row to retained source evidence. |
| SourceRow | Whole number | Yes | Import process | Provides a fallback identifier when the source has no transaction ID. |
| BankRowID | Text | Calculated | Power Query | Uses source transaction ID or account, file, and row fallback. |
| LedgerRowID | Text | Calculated | Power Query | Uses ledger transaction ID or account, file, and row fallback. |
| MatchMethod | Text | Calculated | Power Query | Exact, Reference, or Fuzzy – human confirmed. |
| Similarity | Decimal | Calculated | Power Query | Informational fuzzy score between zero and one. |
| Decision | Choice text | No | Reviewer input table | Accept Fuzzy, Timing Difference, Bank Error Documented, or Needs Investigation. |
| ExceptionType | Text | No | Reviewer or optional AI suggestion | Classifies unresolved and accepted exceptions. |
| ReviewedBy | Text | Required for decisions | Reviewer | Identifies who made the decision. |
| ReviewedAt | Date and time | Required for decisions | Reviewer | Provides decision timing. |
| Notes | Text | Required for exceptions | Reviewer | Explains evidence and accounting treatment. |
The bank and ledger tables have a one-to-one relationship only after a match has been accepted. Before that point, they are independent transaction sets. The review table links to one bank row, one ledger row, or both. Power Query rejects duplicate active decisions for the same row.
Workflow Statuses and Ownership
| Status | Meaning | Owner | Entry condition | Exit condition | Reminder and escalation |
|---|---|---|---|---|---|
| Draft | Run record exists but setup is incomplete. | Preparer | List item created. | Folder and workbook created. | Notify preparer if still Draft after one business day. |
| Awaiting Files | Run structure exists and source files are required. | Preparer | Setup flow succeeds. | Required exports and balances are loaded. | Daily reminder after the expected close calendar date. |
| In Review | Queries have run and exceptions are being investigated. | Preparer | Imports pass validation. | All exceptions are resolved or documented. | Escalate aged exceptions to the controller according to close policy. |
| Ready for Approval | Preparer asserts that the workbook is complete. | Automation | Preparer changes the SharePoint status. | Summary validation succeeds or fails. | No reminder because validation runs immediately. |
| Blocked | Workbook summary is not approval-ready. | Preparer | Unresolved count is nonzero, difference is nonzero, or summary is missing. | Workbook is corrected, refreshed, and resubmitted. | Notify preparer and controller with the blocking reason. |
| Approval In Progress | Validation passed and approval is pending. | Approver | Approval request created. | Approve, reject, or controlled reassignment. | Reminder after two business days; escalation after four, subject to policy. |
| Rejected | Approver returned the reconciliation. | Preparer | Approval response is Reject. | Issues are corrected and the run is resubmitted. | Immediate notification includes reviewer comments. |
| Approved | Required sign-offs and evidence retention are complete. | Controller | All required approvers respond Approve. | Closed unless formally reopened. | No routine reminders. |
| Automation Failed | A technical action failed. | Automation support owner | Flow catch scope runs. | Retry or manual recovery succeeds. | Immediate support notification and daily unresolved-failure report. |
A record moves backward when an approver rejects it, when a refreshed ledger creates new unmatched items, or when source evidence is replaced. A fuzzy suggestion never advances a record by itself. It becomes a match only when an authorized finance reviewer records an eligible decision.
Step-by-Step Implementation
Step 1: Prepare the Accounts and Permissions
- Confirm that the finance users have authorized access to the required QuickBooks Online reports and bank exports.
- Create a SharePoint site or restricted finance area with a document library named
Finance Reconciliations. - Create security groups for reconciliation preparers, approvers, finance administrators, and read-only auditors.
- Give preparers edit access to active run folders and the run list. Give approvers read access to source evidence and edit access only where approval requires it.
- Restrict approved folders so ordinary preparers cannot silently replace approved evidence. Corrections should create a new version or formally reopened run.
- Create a SharePoint list named
Reconciliation Runsusing the fields defined earlier. - Create a shared finance mailbox or monitored notification address for automation failures. Do not use an employee’s personal mailbox as the only support destination.
- Create Power Automate connections using an authorized automation owner. Where organizational policy permits, use a controlled service identity rather than an employee likely to leave the business.
- Create two test users, one preparer and one approver, with the same permission boundaries as production roles.
- Create a test folder and test list view. Use masked sample transactions rather than live bank information during development.
The required Microsoft subscription must support SharePoint, desktop Excel Power Query, the necessary Power Automate connectors, and approval actions. Licensing differs by tenant and connector, so the implementation team should verify current entitlements rather than assume a named plan includes every feature.
No QuickBooks API credentials are required for the selected file-based design. If direct API extraction is added later, store credentials in an approved secret store and grant read-only accounting access wherever the API permits it.
Step 2: Build the Intake
The workbook template contains four controlled input tables:
tblBankImporttblLedgerImporttblReviewDecisionstblRunControl
| Field | Validation |
|---|---|
| ReconID | Required and equal to the named parameter pReconID. |
| AccountKey | Required controlled value matching the run. |
| TransactionDate | Required valid date. |
| Amount | Required number, with deposits positive and withdrawals negative. |
| Description | Required source description. |
| Reference | Optional source reference. |
| BankTransactionID | Preferred unique source identifier. |
| SourceFileName | Required original export name. |
| SourceRow | Required whole number from the source file. |
The ledger table uses the same fields, replacing BankTransactionID with QBOTransactionID and adding TransactionType.
QuickBooks report layouts can vary based on report choice, customization, locale, and product updates. During implementation, the team maps the actual exported columns to the canonical fields rather than relying on assumed headings. If the export provides separate debit and credit columns for the bank asset account, the canonical signed amount is generally calculated as debit minus credit. The team must verify this convention against known transactions before production use.
Original exports are retained in the SharePoint Source folder. The canonical tables are working data, not replacements for source evidence.
Incomplete submissions are blocked by Power Query validation. Duplicate IDs also stop the refresh. This is preferable to silently discarding malformed transactions.
The run naming standard is:
REC-YYYY-MM-ACCOUNTKEY
Examples:
REC-2026-05-OPERATING
REC-2026-05-PAYROLL
REC-2026-05-MERCHANT
Source file names begin with the same identifier:
REC-2026-05-OPERATING_BANK_ORIGINAL.csv
REC-2026-05-OPERATING_QBO_ORIGINAL.xlsx
REC-2026-05-OPERATING_BANK_STATEMENT.pdf
The bank portal and QuickBooks Online remain responsible for authentication and export access. The intake does not expose an anonymous form, so public spam prevention is unnecessary. SharePoint authentication, file permissions, and allowed file types provide the intake boundary.
Step 3: Create the System of Record
The SharePoint list is the run-level system of record. The Excel workbook is the controlled transaction-level reconciliation artifact.
Create the list with indexed columns for ReconID, Status, StatementEndDate, Preparer, and Approver. Enforce unique values on ReconID where supported by the selected SharePoint column configuration.
Create these filtered views:
- My Open Reconciliations: Preparer equals current user and Status is not Approved.
- Awaiting Approval: Status equals Approval In Progress.
- Blocked Runs: Status equals Blocked.
- Automation Failures: AutomationStatus equals Failed.
- Recently Approved: Status equals Approved and completion date is within the reporting period.
- Overdue: Statement end date is before the configured close deadline and Status is not Approved.
The workbook template should use named cells for:
pReconIDpFuzzyThreshold, initially 0.88 for the representative designpFuzzyMargin, initially 0.05pReferenceDateToleranceDays, initially 3pFuzzyDateToleranceDays, initially 5
These values are policy assumptions, not universal thresholds. They should be tested against historical records. A lower fuzzy threshold increases candidate volume and false-suggestion risk.
The tblRunControl table contains one row with:
- ReconID
- AccountKey
- StatementStartDate
- StatementEndDate
- BankEndingBalance
- QBOEndingBalance
- PreparedBy
- PreparedAt
Query output is loaded to two Excel tables:
tblReconciliationOutput, one row per matched or unmatched transaction resulttblRunSummaryOutput, one row containing readiness and control totals
Users must not type decisions directly into either output table because refresh replaces query output. Decisions belong in tblReviewDecisions, linked by stable bank and ledger row IDs.
Step 4: Connect the Tools
| Source | Destination | Trigger | Authentication | Returned identifier or update |
|---|---|---|---|---|
| QuickBooks Online | Excel ledger import | Monthly controlled export | QuickBooks user access | Source file and ledger transaction reference retained. |
| Bank portal | Excel bank import | Monthly controlled export | Bank user access and applicable multifactor authentication | Source file and bank transaction identifier retained. |
| SharePoint list | SharePoint document library | Run item created | Power Automate SharePoint connection | Folder path, workbook identifier, and workbook link written back to the run. |
| Excel tables | Power Query output | Manual Refresh All in desktop Excel | Workbook and SharePoint user permissions | Result and summary tables replaced with refreshed values. |
| SharePoint run item | Excel summary table | Status changes to Ready for Approval | Power Automate SharePoint and Excel connections | Readiness values loaded into flow variables. |
| Power Automate | Approval service | Readiness validation succeeds | Microsoft 365 identity connection | Approval request ID, outcome, responder, comments, and response date. |
| Approval service | SharePoint | Approval completes | Power Automate connections | Run status updated and evidence file created. |
For source-to-canonical mapping, the implementation records the actual exported heading beside its canonical destination. For example, a bank’s posted-date column maps to TransactionDate, while a QuickBooks debit and credit pair maps to one signed Amount. Mapping is tested using known deposits, payments, reversals, and zero-value lines.
If a source file changes its headings, Power Query produces a missing-column error. The support owner updates the adapter only after confirming that the source meaning has not changed.
Step 5: Build the Core Automation
Automation 1: Create the reconciliation run
- Trigger: A SharePoint Reconciliation Runs item is created.
- Conditions: ReconID, account, dates, preparer, approver, and balances are present; no item has the same ReconID.
- Actions: Create the run folders, copy the Excel template, retrieve file metadata, update the run item, and notify the preparer.
- Fields updated: WorkbookIdentifier, WorkbookLink, Status, AutomationStatus, LastAutomationRun, and ErrorMessage.
- Notification: Send the preparer the run ID, workbook link, and source-folder link.
- Exception: On failure, set AutomationStatus to Failed and Status to Automation Failed.
The folder sequence is:
Finance Reconciliations/
2026/
05/
REC-2026-05-OPERATING/
Source/
Support/
Working/
Approved/
Automation 2: Standardize and match transactions
- Trigger: The preparer selects Refresh All in desktop Excel.
- Conditions: Required tables and named parameters exist; row IDs are unique; values can be converted to required data types.
- Actions: Normalize text, convert amounts to cents, create stable IDs, perform staged matching, merge decisions, and calculate summary controls.
- Fields updated: MatchMethod, Similarity, Status, ExceptionType, adjusted balance, difference, and automation readiness.
- Notification: No automatic message is sent during local refresh. The visible output and query errors inform the preparer.
- Exception: Invalid schemas, duplicate IDs, or conversion errors stop the refresh.
Matching occurs in this exact order:
- Unique exact account, transaction date, and amount.
- Unique normalized reference, account, exact amount, and date tolerance.
- Controlled fuzzy description suggestion among remaining rows with the same account, exact amount, and narrow date tolerance.
- Human confirmation of an eligible fuzzy pair.
- Manual classification of unmatched ledger timing items or documented bank errors.
Once a row is accepted at an earlier stage, it is removed from later candidate pools. This prevents a reference or fuzzy candidate from consuming an already matched transaction.
Automation 3: Validate approval readiness
- Trigger: SharePoint status changes to Ready for Approval.
- Conditions: No approval request exists, the workbook identifier is present, and the run is not already approved.
- Actions: Read
tblRunSummaryOutput, confirm one matching ReconID, and evaluate unresolved count, difference cents, and automation status. - Fields updated: Status becomes Approval In Progress or Blocked; validation values and timestamps are copied to the run item.
- Notification: Blocked runs are returned to the preparer with the control values.
- Exception: Missing tables, locked files, or unreadable workbooks enter the failure scope.
Before submitting, the preparer closes the workbook. This reduces the chance of Excel connector conflicts while Power Automate reads the summary table.
Step 6: Add Approvals, Reminders, and Escalations
The primary approval is sequential. The controller reviews the source files, workbook, control totals, exception decisions, and preparer evidence.
- Power Automate sets Status to
Approval In Progress. - It creates the primary approval assigned to the run’s Approver email address.
- It stores the returned approval identifier immediately.
- If the controller rejects, the flow records comments, changes Status to
Rejected, and returns ownership to the preparer. - If the controller approves, the flow evaluates the secondary-approval policy.
- A finance director approval is required when the run contains a documented bank error, an explicit policy override, or another organization-defined high-risk condition.
- Only after all required approvals succeed does the flow set Status to
Approved.
For a representative policy, reminders are sent after two business days and escalated after four business days. Each business should replace these timings with its financial-close calendar.
A separate scheduled Power Automate flow runs each morning and finds items where Status is Approval In Progress and the approval due date has passed. It sends a reminder without creating a second approval.
Unavailable approvers are handled through a documented delegation field. Before submission, the controller or finance administrator changes the approver to an authorized delegate. If an approval is already active, the support owner cancels or terminates the existing run according to policy, clears the approval request ID with an audit note, assigns the delegate, and resubmits. The process must not create concurrent approvals for the same ReconID.
Approval evidence includes:
- ReconID
- Workbook link and approved-copy link
- Summary control values
- Approval request identifier
- Approver identity
- Outcome
- Comments
- Response timestamp
- Flow run identifier
- Whether secondary approval was required
Step 7: Add Documents and File Management
The SharePoint library uses one folder per run. Original exports are stored in Source, explanatory documents in Support, the active workbook in Working, and final artifacts in Approved.
File names follow the ReconID naming standard. Supporting files add a short evidence type and date, for example:
REC-2026-05-OPERATING_SUPPORT_BANK-FEE_2026-05-31.pdf
REC-2026-05-OPERATING_APPROVAL_EVIDENCE.json
REC-2026-05-OPERATING_FINAL.xlsx
SharePoint version history is enabled for the working and approved areas. Users replace source files only through a documented correction process. When an export must be replaced, the original remains available through version history or is renamed with a superseded suffix.
Power Automate checks that expected source evidence exists before approval. At minimum, the bank export, bank statement, QuickBooks export, working workbook, and any cited exception support must be available.
Large files and unsupported formats are kept outside Excel tables and linked from the run. If a file upload fails, the run remains in Awaiting Files or Blocked. The preparer receives the failed file name and the destination folder but not sensitive content in the notification.
Retention follows the company’s financial-record policy and applicable legal requirements. The implementation does not assume a universal retention period.
Step 8: Add Reporting and Operational Views
The workbook provides these filtered views:
- New unmatched bank transactions
- Unmatched ledger transactions
- Eligible fuzzy suggestions awaiting confirmation
- Invalid review decisions
- Timing differences
- Documented bank errors
- Items awaiting supporting evidence
- Items by exception owner
- Recently matched transactions
- Match counts by method
- Items with repeated amounts
- Automation readiness and control difference
The SharePoint list provides run-level views for overdue reconciliations, blocked submissions, pending approvals, rejections, and automation failures.
Calculated reporting measures include:
- Bank transaction count
- Ledger transaction count
- Exact-match count
- Reference-match count
- Human-confirmed fuzzy count
- Unresolved exception count
- Accepted timing count and net amount
- Documented bank-error count and net amount
- Adjusted bank balance
- QuickBooks ending balance
- Difference in cents
- Elapsed days from statement end to approval
Excel PivotTables can summarize the output for the current run. For cross-period reporting, approved summary rows can be copied to a protected history list or imported into Power BI. The controller owns metric definitions, while the automation support owner maintains refresh and connection health.
Step 9: Add Security and Governance Controls
- Use least-privilege SharePoint groups rather than granting broad site access.
- Do not store full bank account numbers in ReconID, file names, notifications, prompts, or reporting views.
- Restrict source and support folders to authorized finance, audit, and administrative users.
- Disable anonymous sharing and review organization-wide link settings.
- Store flow connections under a controlled identity with a documented backup owner.
- Do not place passwords, API keys, or bank credentials in workbooks or flow actions.
- Review SharePoint and Power Automate activity logs when investigating changes or failures.
- Remove former employees from finance groups, flow ownership, approval assignments, and QuickBooks access promptly.
- Preserve original exports and workbook versions as audit evidence.
- Require human approval for fuzzy matches, bank errors, accounting entries, and final sign-off.
- Use masked sample data in development and testing.
- Prevent approved runs from being overwritten without a reopen process.
- Document threshold, tolerance, and mapping changes with an effective date and approver.
If AI is added, only an organization-approved service may receive data. Prompts must exclude full account numbers, credentials, personal banking information, card data, and unrelated personally identifiable information.
Step 10: Deploy and Test
- Build the SharePoint list, document structure, workbook, queries, and flows in a test area.
- Load at least three historical reconciliations using masked data.
- Include duplicate amounts, date differences, blank references, reversals, deposits, checks, fees, and outstanding transactions.
- Compare automated results with the previously approved reconciliations.
- Review every false candidate and tune the reference and fuzzy tolerances.
- Complete user acceptance testing with the senior accountant and controller.
- Pilot one account for one close period while preserving the prior process as a rollback option.
- After the pilot, activate the remaining accounts in phases.
- Publish a short operating guide covering exports, refresh, exception decisions, approval submission, and recovery.
- Assign a finance process owner and a technical backup owner.
- Monitor every run during the first two close cycles.
- Keep the previous approved workbook template available until the new process has completed the agreed stabilization period.
Rollback consists of pausing the approval flows, returning the SharePoint status to a controlled manual mode, and completing the reconciliation using the retained source exports and prior approved method. No accounting data is lost because the implementation does not modify QuickBooks Online.
Code and Configuration
The core matching logic is implemented in Power Query M. The code below assumes that the workbook already contains the named parameters and four Excel tables described earlier.
In desktop Excel, open the workbook, select the data-query area, open the Power Query editor, create a blank query, open its advanced editor, and paste the first query. Name it qReconciliationResults.
Replace the parameter values in the named cells rather than editing thresholds directly in the query. The code requests access only to the current workbook. SharePoint permissions are applied when the workbook itself is opened or when Power Automate reads it.
Power Query matching query
let
ReconID = Text.Trim(Text.From(
Excel.CurrentWorkbook(){[Name="pReconID"]}[Content]{0}[Column1]
)),
FuzzyThreshold = Number.From(
Excel.CurrentWorkbook(){[Name="pFuzzyThreshold"]}[Content]{0}[Column1]
),
FuzzyMargin = Number.From(
Excel.CurrentWorkbook(){[Name="pFuzzyMargin"]}[Content]{0}[Column1]
),
ReferenceDateToleranceDays = Int64.From(
Excel.CurrentWorkbook(){[Name="pReferenceDateToleranceDays"]}[Content]{0}[Column1]
),
FuzzyDateToleranceDays = Int64.From(
Excel.CurrentWorkbook(){[Name="pFuzzyDateToleranceDays"]}[Content]{0}[Column1]
),
AsText = (value as any) as text =>
if value = null then "" else Text.Trim(Text.From(value)),
NormalizeReference = (value as any) as text =>
Text.Select(Text.Upper(AsText(value)), {"A".."Z", "0".."9"}),
NormalizeDescription = (value as any) as text =>
let
UpperText = Text.Upper(AsText(value)),
Kept = Text.Select(UpperText, {"A".."Z", "0".."9", " "}),
Parts = List.Select(Text.SplitAny(Kept, " "), each _ <> ""),
StopWords = {"THE", "INC", "LLC", "LTD", "LIMITED", "CO", "COMPANY"},
UsefulParts = List.Select(Parts, each not List.Contains(StopWords, _))
in
Text.Combine(UsefulParts, " "),
ToCents = (value as any) as number =>
Int64.From(Number.Round(Number.From(value) * 100, 0, RoundingMode.AwayFromZero)),
BankRequired = {
"ReconID", "AccountKey", "TransactionDate", "Amount", "Description",
"Reference", "BankTransactionID", "SourceFileName", "SourceRow"
},
LedgerRequired = {
"ReconID", "AccountKey", "TransactionDate", "Amount", "Description",
"Reference", "QBOTransactionID", "TransactionType",
"SourceFileName", "SourceRow"
},
ReviewRequired = {
"ReconID", "BankRowID", "LedgerRowID", "Decision", "ExceptionType",
"Notes", "ReviewedBy", "ReviewedAt"
},
BankSource = Excel.CurrentWorkbook(){[Name="tblBankImport"]}[Content],
BankSelected = Table.SelectColumns(BankSource, BankRequired, MissingField.Error),
BankRun = Table.SelectRows(BankSelected, each AsText([ReconID]) = ReconID),
BankTyped = Table.TransformColumns(
BankRun,
{
{"ReconID", each AsText(_), type text},
{"AccountKey", each Text.Upper(AsText(_)), type text},
{"TransactionDate", each Date.From(_), type date},
{"Amount", each Number.From(_), type number},
{"Description", each AsText(_), type text},
{"Reference", each AsText(_), type text},
{"BankTransactionID", each AsText(_), type text},
{"SourceFileName", each AsText(_), type text},
{"SourceRow", each Int64.From(_), Int64.Type}
}
),
BankCents = Table.AddColumn(
BankTyped, "AmountCents", each ToCents([Amount]), Int64.Type
),
BankDescription = Table.AddColumn(
BankCents, "DescriptionNorm", each NormalizeDescription([Description]), type text
),
BankReference = Table.AddColumn(
BankDescription, "ReferenceNorm", each NormalizeReference([Reference]), type text
),
BankID = Table.AddColumn(
BankReference,
"BankRowID",
each
if [BankTransactionID] <> "" then
[AccountKey] & "|B|" & [BankTransactionID]
else
[AccountKey] & "|B|" & [SourceFileName] & "|" & Text.From([SourceRow]),
type text
),
BankKeyed = Table.AddColumn(
BankID,
"ExactKey",
each [AccountKey] & "|" &
Date.ToText([TransactionDate], "yyyyMMdd") & "|" &
Text.From([AmountCents]),
type text
),
BankRefKeyed = Table.AddColumn(
BankKeyed,
"ReferenceKey",
each [AccountKey] & "|" & Text.From([AmountCents]) & "|" & [ReferenceNorm],
type text
),
BankDuplicateIDs = Table.SelectRows(
Table.Group(BankRefKeyed, {"BankRowID"}, {{"Count", each Table.RowCount(_), Int64.Type}}),
each [Count] > 1
),
Bank = if Table.RowCount(BankDuplicateIDs) > 0
then error "Duplicate BankRowID values were found. Correct the source IDs or source-row values."
else BankRefKeyed,
LedgerSource = Excel.CurrentWorkbook(){[Name="tblLedgerImport"]}[Content],
LedgerSelected = Table.SelectColumns(LedgerSource, LedgerRequired, MissingField.Error),
LedgerRun = Table.SelectRows(LedgerSelected, each AsText([ReconID]) = ReconID),
LedgerTyped = Table.TransformColumns(
LedgerRun,
{
{"ReconID", each AsText(_), type text},
{"AccountKey", each Text.Upper(AsText(_)), type text},
{"TransactionDate", each Date.From(_), type date},
{"Amount", each Number.From(_), type number},
{"Description", each AsText(_), type text},
{"Reference", each AsText(_), type text},
{"QBOTransactionID", each AsText(_), type text},
{"TransactionType", each AsText(_), type text},
{"SourceFileName", each AsText(_), type text},
{"SourceRow", each Int64.From(_), Int64.Type}
}
),
LedgerCents = Table.AddColumn(
LedgerTyped, "AmountCents", each ToCents([Amount]), Int64.Type
),
LedgerDescription = Table.AddColumn(
LedgerCents, "DescriptionNorm", each NormalizeDescription([Description]), type text
),
LedgerReference = Table.AddColumn(
LedgerDescription, "ReferenceNorm", each NormalizeReference([Reference]), type text
),
LedgerID = Table.AddColumn(
LedgerReference,
"LedgerRowID",
each
if [QBOTransactionID] <> "" then
[AccountKey] & "|L|" & [QBOTransactionID]
else
[AccountKey] & "|L|" & [SourceFileName] & "|" & Text.From([SourceRow]),
type text
),
LedgerKeyed = Table.AddColumn(
LedgerID,
"ExactKey",
each [AccountKey] & "|" &
Date.ToText([TransactionDate], "yyyyMMdd") & "|" &
Text.From([AmountCents]),
type text
),
LedgerRefKeyed = Table.AddColumn(
LedgerKeyed,
"ReferenceKey",
each [AccountKey] & "|" & Text.From([AmountCents]) & "|" & [ReferenceNorm],
type text
),
LedgerDuplicateIDs = Table.SelectRows(
Table.Group(LedgerRefKeyed, {"LedgerRowID"}, {{"Count", each Table.RowCount(_), Int64.Type}}),
each [Count] > 1
),
Ledger = if Table.RowCount(LedgerDuplicateIDs) > 0
then error "Duplicate LedgerRowID values were found. Correct the source IDs or source-row values."
else LedgerRefKeyed,
BankExactCounts = Table.Group(
Bank, {"ExactKey"}, {{"BankKeyCount", each Table.RowCount(_), Int64.Type}}
),
LedgerExactCounts = Table.Group(
Ledger, {"ExactKey"}, {{"LedgerKeyCount", each Table.RowCount(_), Int64.Type}}
),
ExactJoin0 = Table.NestedJoin(
Bank, {"ExactKey"}, Ledger, {"ExactKey"}, "LedgerMatch", JoinKind.Inner
),
ExactJoin1 = Table.ExpandTableColumn(
ExactJoin0, "LedgerMatch", {"LedgerRowID"}, {"LedgerRowID"}
),
ExactJoin2 = Table.NestedJoin(
ExactJoin1, {"ExactKey"}, BankExactCounts, {"ExactKey"}, "BankCounts", JoinKind.LeftOuter
),
ExactJoin3 = Table.ExpandTableColumn(
ExactJoin2, "BankCounts", {"BankKeyCount"}, {"BankKeyCount"}
),
ExactJoin4 = Table.NestedJoin(
ExactJoin3, {"ExactKey"}, LedgerExactCounts, {"ExactKey"}, "LedgerCounts", JoinKind.LeftOuter
),
ExactJoin5 = Table.ExpandTableColumn(
ExactJoin4, "LedgerCounts", {"LedgerKeyCount"}, {"LedgerKeyCount"}
),
ExactMatches = Table.SelectRows(
ExactJoin5, each [BankKeyCount] = 1 and [LedgerKeyCount] = 1
),
ExactOut0 = Table.SelectColumns(ExactMatches, {"BankRowID", "LedgerRowID"}),
ExactOut1 = Table.AddColumn(ExactOut0, "MatchMethod", each "Exact", type text),
ExactOut = Table.AddColumn(ExactOut1, "MatchConfidence", each 1.0, type number),
BankAfterExact = Table.SelectRows(
Bank, each not List.Contains(ExactOut[BankRowID], [BankRowID])
),
LedgerAfterExact = Table.SelectRows(
Ledger, each not List.Contains(ExactOut[LedgerRowID], [LedgerRowID])
),
RefBankEligible = Table.SelectRows(BankAfterExact, each [ReferenceNorm] <> ""),
RefLedgerEligible = Table.SelectRows(LedgerAfterExact, each [ReferenceNorm] <> ""),
RefJoin0 = Table.NestedJoin(
RefBankEligible,
{"ReferenceKey"},
RefLedgerEligible,
{"ReferenceKey"},
"LedgerMatch",
JoinKind.Inner
),
RefJoin1 = Table.ExpandTableColumn(
RefJoin0,
"LedgerMatch",
{"LedgerRowID", "TransactionDate"},
{"LedgerRowID", "LedgerDate"}
),
RefJoin2 = Table.AddColumn(
RefJoin1,
"DateGapDays",
each Number.Abs(Duration.Days([LedgerDate] - [TransactionDate])),
Int64.Type
),
RefCandidates = Table.SelectRows(
RefJoin2, each [DateGapDays] <= ReferenceDateToleranceDays
),
RefBankCounts = Table.Group(
RefCandidates, {"BankRowID"}, {{"BankCandidateCount", each Table.RowCount(_), Int64.Type}}
),
RefLedgerCounts = Table.Group(
RefCandidates, {"LedgerRowID"}, {{"LedgerCandidateCount", each Table.RowCount(_), Int64.Type}}
),
RefJoin3 = Table.NestedJoin(
RefCandidates, {"BankRowID"}, RefBankCounts, {"BankRowID"}, "BankCounts", JoinKind.LeftOuter
),
RefJoin4 = Table.ExpandTableColumn(
RefJoin3, "BankCounts", {"BankCandidateCount"}, {"BankCandidateCount"}
),
RefJoin5 = Table.NestedJoin(
RefJoin4, {"LedgerRowID"}, RefLedgerCounts, {"LedgerRowID"}, "LedgerCounts", JoinKind.LeftOuter
),
RefJoin6 = Table.ExpandTableColumn(
RefJoin5, "LedgerCounts", {"LedgerCandidateCount"}, {"LedgerCandidateCount"}
),
ReferenceMatches = Table.SelectRows(
RefJoin6, each [BankCandidateCount] = 1 and [LedgerCandidateCount] = 1
),
ReferenceOut0 = Table.SelectColumns(ReferenceMatches, {"BankRowID", "LedgerRowID"}),
ReferenceOut1 = Table.AddColumn(ReferenceOut0, "MatchMethod", each "Reference", type text),
ReferenceOut = Table.AddColumn(ReferenceOut1, "MatchConfidence", each 1.0, type number),
AutoMatches = Table.Combine({ExactOut, ReferenceOut}),
BankAfterReference = Table.SelectRows(
BankAfterExact, each not List.Contains(ReferenceOut[BankRowID], [BankRowID])
),
LedgerAfterReference = Table.SelectRows(
LedgerAfterExact, each not List.Contains(ReferenceOut[LedgerRowID], [LedgerRowID])
),
EmptyFuzzy = #table(
type table [
BankRowID = text,
SuggestedLedgerRowID = text,
Similarity = number,
DateGapDays = number
],
{}
),
FuzzyForBank = (bankRow as record) as table =>
let
Candidates = Table.SelectRows(
LedgerAfterReference,
each
[AccountKey] = bankRow[AccountKey] and
[AmountCents] = bankRow[AmountCents] and
Number.Abs(Duration.Days([TransactionDate] - bankRow[TransactionDate]))
<= FuzzyDateToleranceDays and
[DescriptionNorm] <> ""
),
CandidateColumns = Table.SelectColumns(
Candidates, {"LedgerRowID", "TransactionDate", "DescriptionNorm"}
),
RenamedCandidates = Table.RenameColumns(
CandidateColumns,
{
{"LedgerRowID", "SuggestedLedgerRowID"},
{"TransactionDate", "LedgerDate"},
{"DescriptionNorm", "CandidateText"}
}
),
Search = #table(
type table [SearchText = text],
{{bankRow[DescriptionNorm]}}
),
Joined = if bankRow[DescriptionNorm] = "" or Table.IsEmpty(RenamedCandidates)
then null
else Table.FuzzyNestedJoin(
Search,
{"SearchText"},
RenamedCandidates,
{"CandidateText"},
"Matches",
JoinKind.LeftOuter,
[
IgnoreCase = true,
IgnoreSpace = true,
Threshold = FuzzyThreshold,
NumberOfMatches = 2,
SimilarityColumnName = "Similarity"
]
),
Expanded = if Joined = null
then EmptyFuzzy
else Table.ExpandTableColumn(
Joined,
"Matches",
{"SuggestedLedgerRowID", "LedgerDate", "Similarity"},
{"SuggestedLedgerRowID", "LedgerDate", "Similarity"}
),
Valid = Table.SelectRows(
Expanded,
each [SuggestedLedgerRowID] <> null and [Similarity] <> null
),
WithBankID = Table.AddColumn(
Valid, "BankRowID", each bankRow[BankRowID], type text
),
WithDateGap = Table.AddColumn(
WithBankID,
"DateGapDays",
each Number.Abs(Duration.Days([LedgerDate] - bankRow[TransactionDate])),
Int64.Type
),
Selected = Table.SelectColumns(
WithDateGap,
{"BankRowID", "SuggestedLedgerRowID", "Similarity", "DateGapDays"}
)
in
Selected,
FuzzyNested = Table.AddColumn(
BankAfterReference, "FuzzyCandidates", each FuzzyForBank(_)
),
FuzzyCandidateTables = FuzzyNested[FuzzyCandidates],
FuzzyCandidates = if List.Count(FuzzyCandidateTables) = 0
then EmptyFuzzy
else Table.Combine(FuzzyCandidateTables),
EmptyFuzzyTop = #table(
type table [
BankRowID = text,
SuggestedLedgerRowID = text,
Similarity = number,
RunnerUpSimilarity = number,
DateGapDays = number
],
{}
),
FuzzyTop0 = if Table.IsEmpty(FuzzyCandidates)
then EmptyFuzzyTop
else Table.Group(
FuzzyCandidates,
{"BankRowID"},
{
{
"TopRecord",
each
let
Sorted = Table.Sort(_, {{"Similarity", Order.Descending}}),
First = Sorted{0},
RunnerUp = if Table.RowCount(Sorted) > 1
then Sorted{1}[Similarity]
else null
in
[
SuggestedLedgerRowID = First[SuggestedLedgerRowID],
Similarity = First[Similarity],
RunnerUpSimilarity = RunnerUp,
DateGapDays = First[DateGapDays]
],
type record
}
}
),
FuzzyTop1 = if Table.IsEmpty(FuzzyTop0)
then EmptyFuzzyTop
else Table.ExpandRecordColumn(
FuzzyTop0,
"TopRecord",
{"SuggestedLedgerRowID", "Similarity", "RunnerUpSimilarity", "DateGapDays"},
{"SuggestedLedgerRowID", "Similarity", "RunnerUpSimilarity", "DateGapDays"}
),
SuggestedLedgerCounts = Table.Group(
FuzzyTop1,
{"SuggestedLedgerRowID"},
{{"SuggestedLedgerCount", each Table.RowCount(_), Int64.Type}}
),
FuzzyTop2 = Table.NestedJoin(
FuzzyTop1,
{"SuggestedLedgerRowID"},
SuggestedLedgerCounts,
{"SuggestedLedgerRowID"},
"SuggestionCounts",
JoinKind.LeftOuter
),
FuzzyTop3 = Table.ExpandTableColumn(
FuzzyTop2,
"SuggestionCounts",
{"SuggestedLedgerCount"},
{"SuggestedLedgerCount"}
),
FuzzyTop = Table.AddColumn(
FuzzyTop3,
"SuggestedEligible",
each
[Similarity] >= FuzzyThreshold and
([RunnerUpSimilarity] = null or [Similarity] - [RunnerUpSimilarity] >= FuzzyMargin) and
[SuggestedLedgerCount] = 1,
type logical
),
ReviewSource = Excel.CurrentWorkbook(){[Name="tblReviewDecisions"]}[Content],
ReviewSelected = Table.SelectColumns(ReviewSource, ReviewRequired, MissingField.Error),
ReviewRun = Table.SelectRows(ReviewSelected, each AsText([ReconID]) = ReconID),
ReviewTyped = Table.TransformColumns(
ReviewRun,
{
{"ReconID", each AsText(_), type text},
{"BankRowID", each AsText(_), type text},
{"LedgerRowID", each AsText(_), type text},
{"Decision", each AsText(_), type text},
{"ExceptionType", each AsText(_), type text},
{"Notes", each AsText(_), type text},
{"ReviewedBy", each AsText(_), type text},
{"ReviewedAt", each if _ = null or AsText(_) = "" then null else DateTime.From(_), type datetime}
}
),
ActiveReviews = Table.SelectRows(ReviewTyped, each [Decision] <> ""),
BankReviews0 = Table.SelectRows(ActiveReviews, each [BankRowID] <> ""),
BankReviewDuplicates = Table.SelectRows(
Table.Group(BankReviews0, {"BankRowID"}, {{"Count", each Table.RowCount(_), Int64.Type}}),
each [Count] > 1
),
BankReviews = if Table.RowCount(BankReviewDuplicates) > 0
then error "More than one active review decision exists for a BankRowID."
else BankReviews0,
AcceptedFuzzy0 = Table.SelectRows(BankReviews, each [Decision] = "Accept Fuzzy"),
AcceptedFuzzy1 = Table.NestedJoin(
AcceptedFuzzy0,
{"BankRowID", "LedgerRowID"},
FuzzyTop,
{"BankRowID", "SuggestedLedgerRowID"},
"Suggestion",
JoinKind.LeftOuter
),
AcceptedFuzzy2 = Table.ExpandTableColumn(
AcceptedFuzzy1,
"Suggestion",
{"SuggestedEligible", "Similarity"},
{"SuggestedEligible", "AcceptedSimilarity"}
),
AcceptedLedgerCounts = Table.Group(
AcceptedFuzzy2,
{"LedgerRowID"},
{{"AcceptedLedgerCount", each Table.RowCount(_), Int64.Type}}
),
AcceptedFuzzy3 = Table.NestedJoin(
AcceptedFuzzy2,
{"LedgerRowID"},
AcceptedLedgerCounts,
{"LedgerRowID"},
"AcceptedCounts",
JoinKind.LeftOuter
),
AcceptedFuzzy4 = Table.ExpandTableColumn(
AcceptedFuzzy3,
"AcceptedCounts",
{"AcceptedLedgerCount"},
{"AcceptedLedgerCount"}
),
AcceptedFuzzy = Table.SelectRows(
AcceptedFuzzy4,
each [SuggestedEligible] = true and [AcceptedLedgerCount] = 1
),
BankResult1 = Table.NestedJoin(
Bank,
{"BankRowID"},
AutoMatches,
{"BankRowID"},
"Auto",
JoinKind.LeftOuter
),
BankResult2 = Table.ExpandTableColumn(
BankResult1,
"Auto",
{"LedgerRowID", "MatchMethod", "MatchConfidence"},
{"AutoLedgerRowID", "AutoMatchMethod", "AutoMatchConfidence"}
),
BankResult3 = Table.NestedJoin(
BankResult2,
{"BankRowID"},
FuzzyTop,
{"BankRowID"},
"Fuzzy",
JoinKind.LeftOuter
),
BankResult4 = Table.ExpandTableColumn(
BankResult3,
"Fuzzy",
{"SuggestedLedgerRowID", "Similarity", "RunnerUpSimilarity", "SuggestedEligible"},
{"SuggestedLedgerRowID", "Similarity", "RunnerUpSimilarity", "SuggestedEligible"}
),
BankResult5 = Table.NestedJoin(
BankResult4,
{"BankRowID"},
BankReviews,
{"BankRowID"},
"Review",
JoinKind.LeftOuter
),
BankResult6 = Table.ExpandTableColumn(
BankResult5,
"Review",
{"LedgerRowID", "Decision", "ExceptionType", "Notes", "ReviewedBy", "ReviewedAt"},
{"ReviewLedgerRowID", "Decision", "ReviewExceptionType", "Notes", "ReviewedBy", "ReviewedAt"}
),
BankResult7 = Table.NestedJoin(
BankResult6,
{"BankRowID", "ReviewLedgerRowID"},
AcceptedFuzzy,
{"BankRowID", "LedgerRowID"},
"Accepted",
JoinKind.LeftOuter
),
BankResult8 = Table.ExpandTableColumn(
BankResult7,
"Accepted",
{"LedgerRowID", "AcceptedSimilarity"},
{"AcceptedLedgerRowID", "AcceptedSimilarity"}
),
BankResult9 = Table.AddColumn(
BankResult8,
"FinalLedgerRowID",
each
if [AutoLedgerRowID] <> null then [AutoLedgerRowID]
else if [AcceptedLedgerRowID] <> null then [AcceptedLedgerRowID]
else null,
type text
),
BankResult10 = Table.AddColumn(
BankResult9,
"Status",
each
if [AutoLedgerRowID] <> null then "Matched"
else if [AcceptedLedgerRowID] <> null then "Matched - Reviewed"
else if [Decision] = "Bank Error Documented" then "Accepted Exception"
else if [Decision] = "Accept Fuzzy" then "Invalid Review Decision"
else "Manual Review",
type text
),
BankResult11 = Table.AddColumn(
BankResult10,
"FinalMatchMethod",
each
if [AutoMatchMethod] <> null then [AutoMatchMethod]
else if [AcceptedLedgerRowID] <> null then "Fuzzy - human confirmed"
else null,
type text
),
BankResult12 = Table.AddColumn(
BankResult11,
"FinalConfidence",
each
if [AutoMatchConfidence] <> null then [AutoMatchConfidence]
else if [AcceptedSimilarity] <> null then [AcceptedSimilarity]
else null,
type number
),
BankResult13 = Table.NestedJoin(
BankResult12,
{"FinalLedgerRowID"},
Ledger,
{"LedgerRowID"},
"FinalLedger",
JoinKind.LeftOuter
),
BankResult14 = Table.ExpandTableColumn(
BankResult13,
"FinalLedger",
{"TransactionDate", "AmountCents", "Description"},
{"LedgerDate", "LedgerAmountCents", "LedgerDescription"}
),
BankResult15 = Table.AddColumn(BankResult14, "ResultType", each "Bank", type text),
BankResult16 = Table.AddColumn(
BankResult15,
"OutputExceptionType",
each
if [ReviewExceptionType] <> null and [ReviewExceptionType] <> ""
then [ReviewExceptionType]
else if [SuggestedEligible] = true
then "Fuzzy suggestion"
else if [Status] = "Manual Review"
then "Unmatched bank transaction"
else null,
type text
),
BankOutputRenamed = Table.RenameColumns(
BankResult16,
{
{"TransactionDate", "BankDate"},
{"AmountCents", "BankAmountCents"},
{"Description", "BankDescription"}
}
),
BankOutput = Table.SelectColumns(
BankOutputRenamed,
{
"ReconID", "ResultType", "BankRowID", "FinalLedgerRowID",
"AccountKey", "BankDate", "LedgerDate", "BankAmountCents",
"LedgerAmountCents", "BankDescription", "LedgerDescription",
"Reference", "FinalMatchMethod", "FinalConfidence", "Similarity",
"SuggestedLedgerRowID", "SuggestedEligible", "Status", "Decision",
"OutputExceptionType", "Notes", "ReviewedBy", "ReviewedAt"
}
),
BankOutputFinal = Table.RenameColumns(
BankOutput,
{
{"FinalLedgerRowID", "LedgerRowID"},
{"FinalMatchMethod", "MatchMethod"},
{"FinalConfidence", "MatchConfidence"},
{"OutputExceptionType", "ExceptionType"}
}
),
UsedLedgerIDs = List.RemoveNulls(
List.Combine({AutoMatches[LedgerRowID], AcceptedFuzzy[LedgerRowID]})
),
LedgerOnly0 = Table.SelectRows(
Ledger, each not List.Contains(UsedLedgerIDs, [LedgerRowID])
),
LedgerOnlyReviews = Table.SelectRows(
ActiveReviews, each [BankRowID] = "" and [LedgerRowID] <> ""
),
LedgerOnlyReviewDuplicates = Table.SelectRows(
Table.Group(
LedgerOnlyReviews,
{"LedgerRowID"},
{{"Count", each Table.RowCount(_), Int64.Type}}
),
each [Count] > 1
),
ValidLedgerOnlyReviews = if Table.RowCount(LedgerOnlyReviewDuplicates) > 0
then error "More than one active ledger-only decision exists for a LedgerRowID."
else LedgerOnlyReviews,
LedgerOnly1 = Table.NestedJoin(
LedgerOnly0,
{"LedgerRowID"},
ValidLedgerOnlyReviews,
{"LedgerRowID"},
"Review",
JoinKind.LeftOuter
),
LedgerOnly2 = Table.ExpandTableColumn(
LedgerOnly1,
"Review",
{"Decision", "ExceptionType", "Notes", "ReviewedBy", "ReviewedAt"},
{"Decision", "ExceptionType", "Notes", "ReviewedBy", "ReviewedAt"}
),
LedgerOnly3 = Table.AddColumn(
LedgerOnly2,
"Status",
each if [Decision] = "Timing Difference"
then "Accepted Exception"
else "Manual Review",
type text
),
LedgerOnly4 = Table.AddColumn(LedgerOnly3, "ResultType", each "Ledger", type text),
LedgerOnly5 = Table.AddColumn(LedgerOnly4, "BankRowID", each null, type text),
LedgerOnly6 = Table.AddColumn(LedgerOnly5, "BankDate", each null, type date),
LedgerOnly7 = Table.AddColumn(LedgerOnly6, "BankAmountCents", each null, Int64.Type),
LedgerOnly8 = Table.AddColumn(LedgerOnly7, "BankDescription", each null, type text),
LedgerOnly9 = Table.AddColumn(LedgerOnly8, "MatchMethod", each null, type text),
LedgerOnly10 = Table.AddColumn(LedgerOnly9, "MatchConfidence", each null, type number),
LedgerOnly11 = Table.AddColumn(LedgerOnly10, "Similarity", each null, type number),
LedgerOnly12 = Table.AddColumn(
LedgerOnly11, "SuggestedLedgerRowID", each null, type text
),
LedgerOnly13 = Table.AddColumn(
LedgerOnly12, "SuggestedEligible", each false, type logical
),
LedgerOutputRenamed = Table.RenameColumns(
LedgerOnly13,
{
{"TransactionDate", "LedgerDate"},
{"AmountCents", "LedgerAmountCents"},
{"Description", "LedgerDescription"}
}
),
LedgerOutput = Table.SelectColumns(
LedgerOutputRenamed,
{
"ReconID", "ResultType", "BankRowID", "LedgerRowID",
"AccountKey", "BankDate", "LedgerDate", "BankAmountCents",
"LedgerAmountCents", "BankDescription", "LedgerDescription",
"Reference", "MatchMethod", "MatchConfidence", "Similarity",
"SuggestedLedgerRowID", "SuggestedEligible", "Status", "Decision",
"ExceptionType", "Notes", "ReviewedBy", "ReviewedAt"
}
),
Combined = Table.Combine({BankOutputFinal, LedgerOutput}),
Sorted = Table.Sort(
Combined,
{
{"Status", Order.Ascending},
{"BankDate", Order.Ascending},
{"LedgerDate", Order.Ascending}
}
)
in
Sorted
Load this query to an Excel table and name the resulting table tblReconciliationOutput.
The query intentionally fails when required columns or row identifiers are invalid. Inspect the error in the Power Query editor. The most likely issues are changed source headings, text in an amount field, invalid dates, duplicated source IDs, or duplicate review decisions.
Power Query summary query
Create a second blank query named qRunSummary and paste this code. It references qReconciliationResults and the run-control table.
let
Results = qReconciliationResults,
ReconID = Text.Trim(Text.From(
Excel.CurrentWorkbook(){[Name="pReconID"]}[Content]{0}[Column1]
)),
ToCents = (value as any) as number =>
Int64.From(Number.Round(Number.From(value) * 100, 0, RoundingMode.AwayFromZero)),
ControlSource = Excel.CurrentWorkbook(){[Name="tblRunControl"]}[Content],
ControlRun = Table.SelectRows(
ControlSource,
each Text.Trim(Text.From([ReconID])) = ReconID
),
CheckedControl = if Table.RowCount(ControlRun) <> 1
then error "tblRunControl must contain exactly one row for the active ReconID."
else ControlRun,
Control = CheckedControl{0},
BankEndingBalanceCents = ToCents(Control[BankEndingBalance]),
QBOEndingBalanceCents = ToCents(Control[QBOEndingBalance]),
TimingRows = Table.SelectRows(
Results,
each
[ResultType] = "Ledger" and
[Status] = "Accepted Exception" and
[Decision] = "Timing Difference"
),
TimingNetCents = List.Sum(List.RemoveNulls(TimingRows[LedgerAmountCents])),
BankErrorRows = Table.SelectRows(
Results,
each
[ResultType] = "Bank" and
[Status] = "Accepted Exception" and
[Decision] = "Bank Error Documented"
),
BankErrorNetCents = List.Sum(List.RemoveNulls(BankErrorRows[BankAmountCents])),
AdjustedBankBalanceCents =
BankEndingBalanceCents + TimingNetCents - BankErrorNetCents,
DifferenceCents = AdjustedBankBalanceCents - QBOEndingBalanceCents,
UnresolvedRows = Table.SelectRows(
Results,
each [Status] = "Manual Review" or [Status] = "Invalid Review Decision"
),
UnresolvedCount = Table.RowCount(UnresolvedRows),
MatchedCount = Table.RowCount(
Table.SelectRows(Results, each Text.StartsWith([Status], "Matched"))
),
ExactMatchCount = Table.RowCount(
Table.SelectRows(Results, each [MatchMethod] = "Exact")
),
ReferenceMatchCount = Table.RowCount(
Table.SelectRows(Results, each [MatchMethod] = "Reference")
),
FuzzyConfirmedCount = Table.RowCount(
Table.SelectRows(Results, each [MatchMethod] = "Fuzzy - human confirmed")
),
BankErrorCount = Table.RowCount(BankErrorRows),
TimingDifferenceCount = Table.RowCount(TimingRows),
AutomationStatus = if UnresolvedCount = 0 and DifferenceCents = 0
then "Ready"
else "Blocked",
Output = Table.FromRecords(
{
[
ReconID = ReconID,
BankEndingBalanceCents = BankEndingBalanceCents,
TimingNetCents = TimingNetCents,
BankErrorNetCents = BankErrorNetCents,
AdjustedBankBalanceCents = AdjustedBankBalanceCents,
QBOEndingBalanceCents = QBOEndingBalanceCents,
DifferenceCents = DifferenceCents,
UnresolvedCount = UnresolvedCount,
MatchedCount = MatchedCount,
ExactMatchCount = ExactMatchCount,
ReferenceMatchCount = ReferenceMatchCount,
FuzzyConfirmedCount = FuzzyConfirmedCount,
TimingDifferenceCount = TimingDifferenceCount,
BankErrorCount = BankErrorCount,
AutomationStatus = AutomationStatus,
LastCalculatedAt = DateTimeZone.UtcNow()
]
}
)
in
Output
Load this query to a one-row Excel table named tblRunSummaryOutput. Test it by changing one ending balance by one cent. The automation status should become Blocked.
Power Automate approval configuration
Create an automated cloud flow triggered by a modified SharePoint list item. Add a trigger condition so ordinary flow-written updates do not start duplicate approval runs. The exact internal column name may vary.
@and(
equals(triggerBody()?['Status']?['Value'], 'Ready for Approval'),
empty(triggerBody()?['ApprovalRequestID'])
)
Configure trigger concurrency to one run at a time for the same process if the tenant’s flow settings permit it. The flow uses these scopes:
- Initialize: Set ReconID, workbook identifier, preparer email, approver email, and run item ID variables.
- Try: Read and validate the workbook summary, create approvals, process outcomes, and retain evidence.
- Catch: Run after Try has failed, timed out, or been skipped due to failure. Update the run to Automation Failed and notify support.
- Finally: Run after both success and failure paths. Update LastAutomationRun and write a sanitized run reference.
Use the Excel Online action that lists rows from a table, with the file identifier stored in the SharePoint run and table name tblRunSummaryOutput. Extract the single summary record with:
first(body('List_rows_present_in_a_table')?['value'])
Create variables using these expressions:
int(first(body('List_rows_present_in_a_table')?['value'])?['UnresolvedCount'])
int(first(body('List_rows_present_in_a_table')?['value'])?['DifferenceCents'])
first(body('List_rows_present_in_a_table')?['value'])?['AutomationStatus']
first(body('List_rows_present_in_a_table')?['value'])?['ReconID']
The validation condition is:
@and(
equals(variables('SummaryReconID'), variables('ReconID')),
equals(variables('AutomationStatus'), 'Ready'),
equals(variables('UnresolvedCount'), 0),
equals(variables('DifferenceCents'), 0)
)
If false, update the run to Blocked and notify the preparer. If true, set Approval In Progress and start the primary approval.
Store the approval output using dynamic values supplied by the approval action. Interface labels can vary, but the underlying values required are outcome, response collection, responder identity, response date, comments, and approval identifier.
Configure transient SharePoint and Excel actions with a limited retry policy for temporary server errors and throttling. Do not retry validation failures or missing-table errors as though they were transient. The Catch scope records those as manual recovery items.
After final approval, create an evidence file containing a JSON object similar to this:
{
"reconId": "REC-2026-05-OPERATING",
"workbookUrl": "YOUR_WORKBOOK_URL",
"approvedCopyUrl": "YOUR_APPROVED_COPY_URL",
"differenceCents": 0,
"unresolvedCount": 0,
"primaryApprovalId": "YOUR_APPROVAL_ID",
"primaryApprover": "YOUR_EMAIL_ADDRESS",
"primaryOutcome": "Approve",
"primaryComments": "Reviewed source files, timing items, and control totals.",
"primaryResponseDateUtc": "2026-06-04T15:42:18Z",
"secondaryApprovalRequired": false,
"flowRunId": "YOUR_FLOW_RUN_ID",
"evidenceCreatedUtc": "2026-06-04T15:43:02Z"
}
Test the flow with a copied workbook and test users. Confirm that it blocks a one-cent difference, an unresolved item, an incorrect ReconID, a missing table, and a duplicate approval request.
Failure Handling and Operational Reliability
| Failure | User-visible result | Automated response | Manual recovery | Owner |
|---|---|---|---|---|
| Missing required source column | Power Query refresh error | No partial output is approved. | Inspect the source layout, update the controlled mapping, and rerun. | Technical owner with finance validation |
| Invalid date or amount | Conversion error identifies the query step | Refresh stops. | Correct the canonical input after checking source evidence. | Preparer |
| Duplicate transaction ID | Explicit duplicate-ID error | Matching does not continue. | Determine whether the source duplicated a row or reused an identifier. | Preparer |
| Duplicate event or status update | No second approval should appear | Trigger condition and stored approval ID block another request. | Cancel duplicate approval if a race condition bypasses the guard, then document recovery. | Automation owner |
| Ambiguous exact match | Transactions remain unmatched | Only one-to-one keys are accepted. | Review references, dates, and descriptions. | Preparer |
| Ambiguous fuzzy suggestion | SuggestedEligible is false | Runner-up margin and collision checks block acceptance. | Investigate manually and document the decision. | Preparer |
| Invalid fuzzy acceptance | Status becomes Invalid Review Decision | Approval readiness remains blocked. | Remove or correct the decision row and refresh. | Preparer |
| Failed workbook read | Run becomes Automation Failed | Catch scope logs the failure and notifies support. | Close the workbook, confirm table names and permissions, then retry. | Automation owner |
| Expired or broken connection | Flow action reports authentication failure | Run is marked failed; blind retries are limited. | Reauthorize the controlled connection and rerun from the failed stage or resubmit. | Microsoft 365 administrator |
| Unavailable approver | Approval remains pending | Reminder and escalation flow identifies the aged request. | Apply authorized delegation and restart without creating concurrent requests. | Controller or finance administrator |
| Failed file copy | Approved run lacks final copy | Approval completion enters the Catch scope rather than claiming full completion. | Copy the versioned workbook and regenerate evidence. | Automation owner |
| Notification failure | Status may be correct but email is absent | Business state remains in SharePoint; notification failure is logged. | Use the run list as the source of truth and resend the notification. | Automation owner |
| Rate limit or timeout | Flow action is delayed or fails | Limited retry with increasing intervals for transient operations. | Retry after service recovery and reconcile flow state against the run item. | Automation owner |
Idempotency means the same event can be processed again without creating a second business result. The implementation achieves this with a unique ReconID, deterministic transaction row IDs, one active approval request ID, status-based trigger conditions, and evidence file names based on ReconID.
The SharePoint run item functions as the operational failure queue. Items with AutomationStatus equal to Failed are reviewed daily during close. The ErrorMessage field contains a sanitized description, while full connector diagnostics remain in restricted flow run history.
Reconciliation between systems occurs at two levels:
- Power Query confirms that every imported bank and ledger row is represented in an output status.
- Power Automate confirms that the summary ReconID equals the SharePoint ReconID before approval.
Manual recovery never involves editing calculated query output. Users correct inputs, decisions, permissions, or connections, then rerun the controlled process.
A Complete Example
For May 2026, the senior accountant creates the operating-account run. Power Automate generates REC-2026-05-OPERATING, creates the SharePoint folders, copies the workbook template, and updates the list item with the workbook identifier.
The bank export contains 438 transactions. One row is:
ReconID: REC-2026-05-OPERATING
AccountKey: OPERATING
TransactionDate: 2026-05-14
Amount: -1842.60
Description: ACH PMT HARBOR SUPPLY INV 88421
Reference: 88421
BankTransactionID: BANK-580274
SourceFileName: REC-2026-05-OPERATING_BANK_ORIGINAL.csv
SourceRow: 147
The QuickBooks export contains 421 ledger transactions. The related row is:
ReconID: REC-2026-05-OPERATING
AccountKey: OPERATING
TransactionDate: 2026-05-13
Amount: -1842.60
Description: Harbor Supply Co Bill Payment
Reference: INV-88421
QBOTransactionID: LEDGER-713984
TransactionType: Bill Payment
SourceFileName: REC-2026-05-OPERATING_QBO_ORIGINAL.xlsx
SourceRow: 132
Power Query creates these row identifiers:
- BankRowID:
OPERATING|B|BANK-580274 - LedgerRowID:
OPERATING|L|LEDGER-713984
The exact match fails because the transaction dates differ by one day. Reference normalization converts 88421 and INV-88421 to different normalized values if the prefix remains significant. During implementation, Harborstone’s approved reference adapter strips the known INV prefix before the general query, producing 88421 on both sides.
The reference stage then evaluates:
- Account keys are equal.
- Signed amounts are both negative 184,260 cents.
- Normalized references are equal.
- Date difference is one day, within the three-day tolerance.
- Only one bank row and one ledger row satisfy the candidate rule.
The output classifies the pair as Matched with method Reference. No human fuzzy decision is needed.
A separate ledger payment of negative 326.40 has not cleared the bank by May 31. It appears as a ledger-only exception. The senior accountant checks the payment support and records:
Decision: Timing Difference
ExceptionType: Outstanding payment
Notes: Payment issued May 30 and not present on the May bank statement.
ReviewedBy: senior.accountant@YOUR_DOMAIN
ReviewedAt: 2026-06-03 14:20
Power Query adds negative 32,640 cents to the bank ending balance as an outstanding ledger item. The adjusted bank balance then equals the QuickBooks ending balance, and no unresolved transactions remain.
The preparer changes the run to Ready for Approval. Power Automate reads the summary:
{
"ReconID": "REC-2026-05-OPERATING",
"DifferenceCents": 0,
"UnresolvedCount": 0,
"ExactMatchCount": 351,
"ReferenceMatchCount": 72,
"FuzzyConfirmedCount": 5,
"TimingDifferenceCount": 10,
"BankErrorCount": 0,
"AutomationStatus": "Ready"
}
The controller receives the approval, opens the workbook and evidence folder, reviews the timing items and control totals, and approves. Power Automate stores the returned approval identifier, copies the approved workbook, creates the JSON evidence file, and changes the run status to Approved.
If the ledger timing item had no review note, or if the adjusted balance differed by one cent, Power Automate would have blocked the request before the controller received an approval.
Implementation Cost
The following amounts are representative planning assumptions, not vendor quotes or verified client costs. Existing QuickBooks Online and Microsoft 365 subscriptions are treated as existing business systems. Each organization must verify current licensing, connector requirements, tax, and regional pricing.
| Item | Assumption | Estimated amount |
|---|---|---|
| Process design and source mapping | Included in 60 professional hours | Included below |
| Workbook and Power Query build | Included in 60 professional hours | Included below |
| SharePoint and Power Automate build | Included in 60 professional hours | Included below |
| Professional implementation labour | 60 hours at a representative 135 per hour | 8,100 |
| Internal process workshops | 6 hours at a loaded 48 per hour | 288 |
| Testing and user acceptance | 12 hours at a loaded 48 per hour | 576 |
| Training | 6 hours at a loaded 48 per hour | 288 |
| Documentation review | 4 hours at a loaded 48 per hour | 192 |
| Total representative implementation cost | Professional and internal labour | 9,444 |
| Item | Classification | Monthly assumption |
|---|---|---|
| Existing QuickBooks Online subscription | Existing cost | Excluded from incremental estimate |
| Existing Microsoft 365 and Excel access | Existing cost | Excluded from incremental estimate |
| Storage and automation allocation | Representative internal budget allowance | 35 |
| Operational maintenance labour | Internal labour | 2 hours, included in savings calculation |
| Optional AI usage allowance | Optional | 10 |
| Optional advanced reporting | Optional | Depends on existing reporting licenses and scope |
A business implementing the system entirely with internal staff may not incur the representative professional fee, but it should still account for design, construction, testing, training, documentation, and maintenance time.
Estimated Time and Cost Savings
The calculation uses these representative assumptions:
| Assumption | Value |
|---|---|
| Monthly bank transaction volume | 1,250 |
| Current handling time | 1.6 minutes per bank transaction |
| New routine handling time | 0.30 minutes per bank transaction |
| Exception rate | 8 percent, or 100 transactions |
| Exception review time | 4 minutes per exception |
| Monthly maintenance | 2 hours |
| Loaded finance labour cost | 48 per hour |
| Recurring incremental tool allowance | 35 per month |
| One-time implementation cost | 9,444 |
Current monthly labour hours: Monthly volume × current minutes per record ÷ 60
1,250 × 1.6 ÷ 60 = 33.33 hours
New monthly labour hours: Monthly volume × new minutes per record ÷ 60, plus exception handling and maintenance
(1,250 × 0.30 ÷ 60) + (100 × 4 ÷ 60) + 2 = 6.25 + 6.67 + 2 = 14.92 hours
Monthly hours recovered: Current monthly labour hours minus new monthly labour hours
33.33 - 14.92 = 18.41 hours
Estimated monthly labour value: Monthly hours recovered × loaded hourly labour cost
18.41 × 48 = 883.68
Net estimated monthly value: Monthly labour value minus recurring tool costs
883.68 - 35 = 848.68
Estimated payback period: One-time implementation cost ÷ net estimated monthly value
9,444 ÷ 848.68 = approximately 11.1 months
Recovered time does not automatically reduce payroll. It may provide additional close capacity, quicker investigation, reduced overtime, less dependence on one preparer, and the ability to handle higher transaction volume without adding the same amount of administrative work.
Non-financial benefits include:
- Clear separation between deterministic matches and human decisions
- Fewer email follow-ups
- Consistent amount and reference normalization
- Better visibility into unresolved items
- Repeatable one-to-one matching controls
- Durable approval evidence
- Faster controller review
- Better reporting on recurring exceptions
- Reduced risk of approving an incomplete reconciliation
Readers should replace volume, handling time, exception rate, review time, labour rate, maintenance, software allocation, and implementation cost with their own measured figures.
Adding AI to the Automation
AI is optional and should be added only after the deterministic process works reliably. Most reconciliation controls do not require AI.
Required fields, exact amount comparison, date tolerances, normalized references, one-to-one constraints, approval thresholds, permissions, and balance calculations should remain rule-based. AI is not more reliable than these deterministic controls.
Potential AI uses include:
- Summarizing long bank and ledger descriptions
- Suggesting an exception category
- Identifying likely missing information in reviewer notes
- Drafting a short investigation checklist
- Grouping recurring description patterns for reporting
- Suggesting whether an item resembles a timing difference, duplicate, fee, or unknown transaction
The core automation creates standardized records, deterministic matches, exceptions, workflow status, approvals, and evidence. AI contributes only to reading and categorizing unstructured exception information.
The Recommended AI Enhancement
The recommended enhancement is AI-assisted exception triage. It runs only for unresolved transactions after exact and reference matching.
- Trigger: A reconciliation enters In Review and unresolved exception rows are available.
- AI input: Masked bank description, masked ledger candidate descriptions, signed amount, date gap, reference-presence flags, prior match results, and permitted exception history.
- Expected output: Summary, suggested category, suggested next action, missing-information list, evidence rationale, and confidence.
- Record update: Write the suggestion to a separate SharePoint exception-triage list keyed by ReconID and row ID.
- Human review: A finance reviewer accepts, changes, or ignores the suggestion.
- Low confidence: Confidence below 0.75 is displayed as no recommendation.
- Prohibited data: Full bank account numbers, credentials, card data, personal banking details, unnecessary personal information, and confidential information outside the exception.
- Failure behavior: Leave the exception unclassified and continue with manual review.
The reusable system instruction is:
You assist a finance employee with bank-reconciliation exception triage.
Use only the supplied transaction facts. Do not invent transactions, documents, accounting entries, approvals, or policies.
You may suggest an exception category and next investigation step. You must not approve a match, post an accounting entry, conclude that fraud occurred, or make a final accounting decision.
An exact amount does not prove that two transactions match. Treat missing references, repeated amounts, conflicting dates, and conflicting descriptions as uncertainty.
Return only JSON matching the provided schema. If the evidence is insufficient, use category unknown, select obtain_supporting_evidence as the next action, and explain what is missing.
The user prompt template is:
Reconciliation ID: {{RECON_ID}}
Account key: {{MASKED_ACCOUNT_KEY}}
Bank row ID: {{BANK_ROW_ID_OR_NULL}}
Ledger row ID: {{LEDGER_ROW_ID_OR_NULL}}
Bank date: {{BANK_DATE_OR_NULL}}
Ledger date: {{LEDGER_DATE_OR_NULL}}
Signed amount in cents: {{AMOUNT_CENTS}}
Bank description: {{MASKED_BANK_DESCRIPTION}}
Ledger description: {{MASKED_LEDGER_DESCRIPTION}}
Bank reference present: {{TRUE_OR_FALSE}}
Ledger reference present: {{TRUE_OR_FALSE}}
Date gap in days: {{DATE_GAP_OR_NULL}}
Exact match result: {{RESULT}}
Reference match result: {{RESULT}}
Fuzzy similarity, if calculated: {{SCORE_OR_NULL}}
Current reviewer notes: {{MASKED_NOTES_OR_EMPTY}}
Classify the exception and recommend the next investigation action. Do not approve or reject the transaction.
Use this strict output schema:
{
"type": "object",
"additionalProperties": false,
"properties": {
"summary": {
"type": "string",
"maxLength": 400
},
"category": {
"type": "string",
"enum": [
"timing_difference_candidate",
"missing_ledger_entry_candidate",
"duplicate_candidate",
"bank_error_candidate",
"description_mismatch",
"amount_mismatch",
"date_mismatch",
"missing_information",
"unknown"
]
},
"recommended_next_action": {
"type": "string",
"enum": [
"check_bank_support",
"check_quickbooks_entry",
"compare_source_references",
"check_for_duplicate",
"request_owner_explanation",
"obtain_supporting_evidence",
"manual_finance_review"
]
},
"missing_information": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 6
},
"evidence_rationale": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 6
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"requires_human_review": {
"type": "boolean",
"const": true
}
},
"required": [
"summary",
"category",
"recommended_next_action",
"missing_information",
"evidence_rationale",
"confidence",
"requires_human_review"
]
}
If an approved OpenAI API connection is used, Power Automate can call the Responses API through an authorized HTTP connection. Replace placeholders and select a currently supported model that accepts structured output.
{
"model": "YOUR_APPROVED_MODEL",
"instructions": "You assist a finance employee with bank-reconciliation exception triage. Use only supplied facts. Do not approve matches, post entries, allege fraud, or make final accounting decisions. Return only JSON matching the schema.",
"input": "Reconciliation ID: REC-2026-05-OPERATING\nAccount key: OPERATING\nBank row ID: OPERATING|B|BANK-580274\nLedger row ID: null\nBank date: 2026-05-14\nLedger date: null\nSigned amount in cents: -184260\nBank description: ACH PMT SUPPLIER INV 88421\nLedger description: null\nBank reference present: true\nLedger reference present: false\nDate gap in days: null\nExact match result: no unique match\nReference match result: no unique match\nFuzzy similarity: null\nCurrent reviewer notes: none",
"text": {
"format": {
"type": "json_schema",
"name": "reconciliation_exception_triage",
"strict": true,
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"summary": {"type": "string", "maxLength": 400},
"category": {
"type": "string",
"enum": [
"timing_difference_candidate",
"missing_ledger_entry_candidate",
"duplicate_candidate",
"bank_error_candidate",
"description_mismatch",
"amount_mismatch",
"date_mismatch",
"missing_information",
"unknown"
]
},
"recommended_next_action": {
"type": "string",
"enum": [
"check_bank_support",
"check_quickbooks_entry",
"compare_source_references",
"check_for_duplicate",
"request_owner_explanation",
"obtain_supporting_evidence",
"manual_finance_review"
]
},
"missing_information": {
"type": "array",
"items": {"type": "string"},
"maxItems": 6
},
"evidence_rationale": {
"type": "array",
"items": {"type": "string"},
"maxItems": 6
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"requires_human_review": {
"type": "boolean",
"const": true
}
},
"required": [
"summary",
"category",
"recommended_next_action",
"missing_information",
"evidence_rationale",
"confidence",
"requires_human_review"
]
}
}
}
}
The HTTP request uses POST, an authorization bearer header containing YOUR_API_KEY, and a JSON content type. Credentials must be stored in an approved connection or secret-management facility, not directly in visible flow steps.
Power Automate parses the returned JSON, validates every enum and the confidence range, and writes the result to the triage list. A malformed response, HTTP 400 response, or schema failure goes to manual review without changing reconciliation status. HTTP 429 and temporary server failures receive a limited retry with increasing delay.
Log model identifier, request timestamp, row ID, response status, token or usage information when available, and reviewer disposition. Do not log prohibited source data in general-purpose flow messages.
Benefits of the AI Enhancement
- Less time reading repetitive bank descriptions
- More consistent initial exception categories
- Faster identification of missing references or support
- Shorter summaries for controller review
- Improved reporting on recurring exception themes
- Better prioritization of unfamiliar or incomplete transactions
These are AI-specific benefits. Standardized imports, exact matching, reference matching, one-to-one controls, status management, approval routing, and evidence retention already exist without AI.
What Remains Rule-Based or Human-Controlled
| Decision | Control type | Reason |
|---|---|---|
| Amount equality | Rule-based | Integer-cent comparison is more reliable than language-model judgment. |
| Date tolerance | Rule-based | The permitted range is an accounting policy setting. |
| Unique-match enforcement | Rule-based | A transaction must not be consumed more than once. |
| Fuzzy match acceptance | Human-controlled | Description similarity does not establish transaction identity. |
| Timing-difference classification | Human-controlled | The reviewer must inspect payment status and supporting evidence. |
| Bank-error conclusion | Human-controlled | It may require bank communication and higher-level approval. |
| QuickBooks entry | Human-controlled under accounting permissions | The reconciliation must not post accounting entries automatically. |
| Final reconciliation approval | Human-controlled | The controller remains accountable for sign-off. |
| Fraud or legal conclusion | Human-controlled | AI triage is not an investigative or legal determination. |
Estimating the Additional Value of AI
The representative core automation produces approximately 100 exceptions per month. Without AI, each exception takes an average of four minutes to read, categorize, investigate, and document.
| Process | Monthly exception effort | Control position |
|---|---|---|
| Original manual process | Included in approximately 33.33 total reconciliation hours | Informal spreadsheet and email review |
| Core automation without AI | 100 × 4 minutes = 6.67 hours | Structured exceptions and human review |
| Core automation with AI triage | 100 × 3 minutes = 5.00 hours | AI suggestion with mandatory human review |
Additional monthly capacity: 6.67 - 5.00 = 1.67 hours
Representative labour value: 1.67 × 48 = 80.16
Less representative AI usage allowance: 80.16 - 10 = 70.16 per month
The estimate assumes all 100 outputs are reviewed, approximately 15 percent require category correction, and approximately 2 percent fail or return no useful recommendation. AI does not eliminate review or guarantee accuracy.
Testing Checklist
Use masked sample data before processing real financial information.
| Test | Expected result |
|---|---|
| Normal submission | Run, folders, workbook, and notification are created once. |
| Missing required field | Run setup or query refresh is blocked with a clear error. |
| Invalid date or amount | Power Query stops before matching. |
| Duplicate transaction ID | Explicit duplicate-ID error is produced. |
| Duplicate run submission | Unique ReconID control prevents a second business run. |
| Duplicate modified event | No second approval request is created. |
| Unique exact match | Transaction is classified as Exact. |
| Repeated exact key | Transactions remain unmatched rather than being paired arbitrarily. |
| Unique reference match within tolerance | Transaction is classified as Reference. |
| Reference outside tolerance | No automatic match occurs. |
| Eligible fuzzy suggestion | Candidate appears but remains unaccepted. |
| Ambiguous fuzzy suggestion | SuggestedEligible is false. |
| Accepted valid fuzzy pair | Status becomes Matched – Reviewed. |
| Accepted invalid fuzzy pair | Status becomes Invalid Review Decision and approval is blocked. |
| Unavailable approver | Reminder and authorized delegation procedure operate correctly. |
| Approval rejection | Status becomes Rejected and comments return to the preparer. |
| Reassignment | Only the authorized delegate receives the replacement request. |
| Overdue item | Scheduled reminder flow identifies it. |
| Escalation | Escalation occurs without creating a duplicate approval. |
| One-cent control difference | AutomationStatus becomes Blocked. |
| Unresolved exception | Approval validation fails. |
| Failed authentication | Run becomes Automation Failed and support is notified. |
| Expired credential | Connection must be reauthorized before controlled retry. |
| Failed API or connector request | Limited retry applies only to transient errors. |
| Failed file upload or copy | Run does not claim complete evidence retention. |
| Failed document creation | Approval completion enters the failure path. |
| Failed notification | SharePoint state remains correct and the failure is logged. |
| Unauthorized user | Source and approved evidence cannot be accessed. |
| Malformed AI output | Schema validation fails and the exception remains manual. |
| Inaccurate AI suggestion | Reviewer corrects it without changing deterministic match logic. |
| AI service failure | Core reconciliation continues without AI. |
| Successful completion | Approved workbook, evidence JSON, timestamps, and status are retained. |
| Correct reporting | Counts reconcile to all imported bank and ledger rows. |
| Correct audit record | Source files, decisions, approvals, and versions are traceable. |
| Correct retry behavior | Retry does not create duplicate runs, files, or approvals. |
Ongoing Maintenance
The controller is the primary business owner. A finance systems administrator or automation specialist is the technical owner, with a named backup for close periods.
| Frequency | Task | Owner |
|---|---|---|
| Daily during close | Review blocked runs, failed flows, pending approvals, and aged exceptions. | Preparer and automation owner |
| Monthly | Reconcile imported row counts, review match-method distribution, and sample approved evidence. | Controller |
| Monthly | Check Power Automate run history, retry patterns, connector failures, and usage allocation. | Automation owner |
| Quarterly | Review SharePoint permissions, approval delegates, flow owners, and former-user access. | Finance administrator |
| Quarterly | Sample fuzzy matches and assess false-suggestion rates before changing thresholds. | Controller |
| Quarterly | Confirm QuickBooks and bank export mappings still represent the same fields and sign conventions. | Senior accountant |
| Semiannually | Test failed authentication, blocked approval, rollback, and manual recovery. | Automation owner |
| Annually | Review retention, audit, privacy, close-policy, and licensing requirements. | Controller and administrator |
| After any change | Update process documentation, query version, test evidence, and effective date. | Change owner |
| Monthly if AI is enabled | Sample outputs, correction rates, service failures, prohibited-data controls, and usage cost. | Finance owner and AI governance owner |
Backups must cover SharePoint content and any tenant-specific recovery requirements. Version history is useful but should not be treated as the only backup strategy without confirming organizational recovery controls.
Thresholds should not be adjusted merely to increase the apparent match rate. Changes require historical testing, documented rationale, and controller approval.
When to Move to Dedicated Software
The Excel and Power Automate design can remain appropriate while volumes, entities, permissions, and exception patterns are manageable. Replacement is not automatic.
Dedicated reconciliation or financial-close software should be considered when:
- Transaction volume makes desktop refresh or workbook review too slow.
- The business operates many bank accounts, entities, currencies, or locations.
- One-to-many and many-to-many matching become common.
- Formal segregation-of-duties controls exceed SharePoint and workbook permissions.
- Regulators or auditors require specialized immutable audit capabilities.
- Direct bank feeds and accounting APIs become mandatory.
- Exception rates remain high despite process correction.
- Workbook and query maintenance consumes excessive technical time.
- Multiple preparers need simultaneous transaction-level review.
- Advanced close orchestration, certification, or intercompany matching is required.
- Customer-facing, mobile, or offline access becomes necessary.
- Vendor support and contractual service levels are required.
- Security risk increases beyond what the file-based architecture can reasonably control.
Before replacing the implementation, the business should determine whether the underlying problem is software capacity, poor source data, inconsistent accounting references, or an unresolved operating process. Dedicated software will not correct weak source discipline by itself.
Implementation Checklist
- Confirm the account scope, statement periods, volumes, owners, and approval policy.
- Document actual bank and QuickBooks export formats.
- Verify the signed-amount convention with known transactions.
- Select Excel, Power Query, SharePoint, and Power Automate roles.
- Confirm licensing and connector requirements.
- Create test and production accounts, connections, groups, and delegates.
- Create the Reconciliation Runs list and indexed operational views.
- Create the SharePoint template, source, support, working, and approved folders.
- Build the canonical bank and ledger import tables.
- Create stable row identifiers and unique ReconID controls.
- Build exact one-to-one matching.
- Build normalized reference matching with a date tolerance.
- Build controlled fuzzy suggestions with threshold, margin, and collision checks.
- Create a separate review-decision table.
- Define timing, bank-error, investigation, and rejection rules.
- Build the run summary and balance-control calculation.
- Load query results into named Excel output tables.
- Create the run-setup Power Automate flow.
- Create readiness validation and approval routing.
- Add sequential secondary approval where policy requires it.
- Add reminders, escalations, unavailable-approver handling, and reassignment controls.
- Add evidence-copy and approval-evidence creation actions.
- Configure Try, Catch, and Finally scopes with run-after behavior.
- Set limited retries for transient errors and manual recovery for validation failures.
- Create blocked, overdue, rejected, failure, and manual-review views.
- Apply least-privilege permissions and shared-link restrictions.
- Test duplicate events, duplicate IDs, invalid inputs, one-cent differences, and failed connections.
- Complete user acceptance testing with preparers and approvers.
- Pilot one account before phased deployment.
- Document implementation cost and replace representative assumptions with actual figures.
- Measure current handling time, exception rate, maintenance, and loaded labour cost.
- Add AI only after the deterministic process is stable.
- Keep fuzzy acceptance, accounting treatment, bank errors, and final approval human-controlled.
- Assign primary and backup maintenance owners.
- Define the volume, security, audit, and complexity criteria that would justify dedicated software.
Department/Function: Finance & Accounting
Get a FREE
Proof of Concept
& Consultation
No Cost, No Commitment!


