We've all been there: You need to import a legacy CSV or e-commerce data into Business Central, but the names don't quite match. "Contoso Ltd" in your file vs "Contoso Limited" in BC. Standard filters fail, and you're stuck manually fixing thousands of lines.
I built VeriMatch to solve this. It's an asynchronous engine that uses fuzzy logic (Levenshtein Distance) to find the best match for your data, giving you a similarity score so you can decide what to merge.
I wanted to create something strictly typed but dynamic enough to handle any table.
- Custom AL Levenshtein Algo: I wrote the math from scratch in AL. It uses a matrix-based approach to calculate exactly how "close" two strings are.
- Completely Dynamic: I used
RecordRefandFieldReffor everything. You can match Customers, Items, Vendors, or custom tables without changing a single line of code. - Performance First: Heavy math in AL can be slow, so I offloaded the processing to the Job Queue. It runs in the background so the UI doesn't freeze.
- Memory Optimization: I added logic to skip expensive calculations if string lengths are wildly different, and I manually handle memory clearing on the matrix arrays to keep the service tier happy.
- Transactional Updates: You approve the matches in a worksheet, and the updates are applied in a single batch.
I architected this using the Handler Pattern to keep my logic clean:
VRM Algorithm Lib: A stateless Codeunit. It just takes two strings and returns a % score. Pure math, no database dependencies.VRM Processor: The heavy lifter. It handles the Job Queue execution, loops through the data, and manages theRecordReflogic.VRM Import Buffer: A flattened table to get data in fast (SQL optimized) before we process it.
- Clone this repo.
- Open in VS Code.
- Download symbols (
Ctrl+Shift+P->AL: Download Symbols). - Hit
F5to publish to your Sandbox/Docker.
Search for "VeriMatch Projects". This is where you define the rules.
- Target Table: Pick where you want data to go (e.g.,
18for Customer). - Match Field: Pick the field we compare names against (e.g.,
2Name). - Threshold: I usually set this to
80. Anything lower is usually noise. - Delimiter: Set this to whatever your CSV uses (Semicolon, Comma, etc.).
Grab your messy CSV file (no headers needed).
1001;Adatum Corp;555-0100
1002;Contoso Ltd.;555-0199Click "Import CSV". The app will import the data based on your Key Column and Delimiter settings.
Tell the system where the data goes. In the Update Mapping subform:
- Example: Take CSV Col
3-> Put it in BC Field9(Phone No.).
Click "Run Analysis". Go grab a coffee ☕. The Job Queue does the work.
When it's done, open the Candidates Worksheet.
- Sort by Score.
- You'll see matches like "Contoso Ltd" = "Contoso Limited" (95%).
- Select the ones you like (Ctrl+Click).
- Hit "Set Selected to Update".
- Click "Execute Updates" to commit the changes.
I also exposed the whole engine via Connect APIs. This is great if you want to push data from a Python script or an external PIM without touching the UI.
Base URL: https://api.businesscentral.dynamics.com/v2.0/{tenant}/sandbox/api/contoso/verimatch/v1.0/companies({id})
POST /projects
{
"code": "API-JOB-01",
"description": "Nightly Customer Sync",
"targetTableId": 18,
"targetFieldId": 2,
"minConfidence": 85,
"csvKeyColumnNo": 1
}POST /fieldMaps
{
"projectCode": "API-JOB-01",
"sourceColumnIndex": 3,
"destTableId": 18,
"destFieldId": 9
}POST /bufferLines
{
"projectCode": "API-JOB-01",
"lineNo": 1,
"searchKey": "Contoso Ltd",
"col1": "Contoso Ltd",
"col2": "555-0100"
}POST /projects({systemId})/Microsoft.NAV.runAnalysis
GET /candidates?$filter=matchScore ge 90
PATCH /candidates({systemId})
{
"userDecision": "Update Record"
}This project is licensed under the MIT License - see the LICENSE file for details.