How to Deduplicate MySQL Customer Records with AI Matching
Build a scheduled Spojit workflow that pulls customer rows from MySQL, uses an Agent-mode Connector node with a Response Schema to detect duplicate identities, and pauses for human approval before any records are merged.
What This Integration Does
Customer tables drift over time: the same person signs up twice with a typo, a different email casing, or a phone number formatted three ways. Exact-match SQL misses these because the duplicates are not byte-for-byte identical. This workflow uses Spojit to read a batch of customer rows, hand them to an Agent-mode Connector node, and ask it to group rows that almost certainly describe the same person. Because merges are destructive, the workflow never writes blindly: it surfaces every proposed merge to a person who approves or rejects it first.
The workflow runs on a Schedule trigger (for example, every weekday morning). On each run it queries MySQL with the mysql connector, reshapes the rows into a compact list, and sends them to an Agent-mode Connector step whose responseSchema forces a strict JSON answer (a list of duplicate clusters with confidence scores). A Human node then pauses the run until an approver acts. Only on approval does the final Direct-mode step apply merges with update-rows. If nobody approves, the run halts and no data changes, so re-running later is safe: the same candidates resurface until they are resolved.
Prerequisites
- A configured MySQL connection in Spojit (host, database, and credentials), set up via Connections → Add connection. The credentials need read access to your customer table and write access if you want this workflow to apply merges.
- A customer table you can name and describe: this tutorial assumes a table called
customerswith columns such asid,email,first_name,last_name, andphone. Adjust the queries to match your schema. - At least one user, role, or team to act as an approver in the Approvals inbox. Approvers need access to the workspace.
- The built-in
jsonconnector is available to every workspace with no setup; you will use it to shape data between steps. - A workspace plan with AI credits available, since Agent mode consumes credits on each run.
Step 1: Add a Schedule trigger
Create a new workflow in the Workflow Designer and set the Trigger node type to Schedule. A Schedule trigger takes a 5-field Unix cron expression plus an IANA timezone. To run every weekday at 9am Sydney time, use:
Cron: 0 9 * * 1-5
Timezone: Australia/Sydney
A single trigger can hold multiple schedules if you want more than one run window. The trigger output is {{ scheduledAt }}, which you can reference downstream if you want to stamp the run time onto records.
Step 2: Pull customer rows with the MySQL connector (Direct mode)
Add a Connector node, choose the MySQL connector, and keep it in Direct mode so the call is deterministic and costs no AI credits. Select the execute-query tool. Put your read query in the query field and pass any values through the params array so they are safely bound rather than concatenated into the SQL. Keep the batch bounded so a single run stays fast:
SELECT id, email, first_name, last_name, phone, created_at
FROM customers
ORDER BY created_at DESC
LIMIT 500;
Name the step output something memorable, for example customers. Downstream you can reference the returned rows as {{ customers.rows }}. If your table is large, narrow the scope further (for example, only rows changed in the last week) so you compare a manageable set each run.
Step 3: Shape the rows into a compact candidate list (json connector)
Sending every column to the AI step wastes tokens and credits. Add a Connector node using the built-in json connector in Direct mode and use the pick tool to keep only the fields that matter for identity matching: email, first_name, last_name, and phone, plus the id so the AI can refer back to each row. Point the input at {{ customers.rows }} and name the output candidates.
If you need to normalize values first (for example lowercasing email or trimming whitespace), you can chain a Transform node here, but the AI step is generally robust to minor formatting differences, so a simple field pick is usually enough.
Step 4: Detect duplicates with an Agent-mode Connector node and a Response Schema
Add a Connector node and switch it to Agent mode. Agent mode lets the agent reason over the whole list and decide which rows belong together, which is exactly the judgment task exact SQL cannot do. In the prompt, describe the task and reference your candidate list with a variable:
You are matching customer records that may be duplicates of the same
real person. Compare these rows on name, email, and phone. Account for
typos, different email casing, nicknames, and reformatted phone numbers.
Group rows that are very likely the same person into clusters. Do not
guess across clearly different people.
Rows:
{{ candidates }}
Turn on Response Schema so the answer comes back as strict JSON every time instead of prose. Define a schema that returns an array of clusters, each holding the row ids it covers, the id you propose to keep, and a confidence score:
{
"type": "object",
"properties": {
"clusters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"keepId": { "type": "integer" },
"duplicateIds": {
"type": "array",
"items": { "type": "integer" }
},
"confidence": { "type": "number" },
"reason": { "type": "string" }
},
"required": ["keepId", "duplicateIds", "confidence"]
}
}
},
"required": ["clusters"]
}
Name the output matches. Downstream you can reference the clusters as {{ matches.clusters }}. The confidence field lets you filter out weak matches before anyone is asked to review them.
Step 5: Pause for human approval before merging
Add a Human node so no merge happens without sign-off. Set a clear Label and Message that summarizes what is being asked, and use variables so the approver sees the actual proposals in their notification:
Label: Review proposed customer merges
Message: The agent found duplicate clusters in the customers table.
Approve to merge them, or reject to leave the records untouched.
Clusters: {{ matches.clusters }}
Fill in the Notification title and Notification body (both accept {{ variables }}), set Urgency to Normal, and add at least one Approval slot with a user, role, or team atom; the slot is the only required field. Optionally enable Email approvers so the first reviewers are emailed, and set a Timeout (minutes) if you want unanswered requests to expire. Approvers respond in the Approvals inbox at /approvals. On approval the node outputs { approved: true, approvalId, outcome: "APPROVED" } and the run continues. A rejection or timeout halts the run with no changes, which is exactly the safe default you want for a destructive operation.
Step 6: Apply the merges with MySQL update-rows (Direct mode)
After the Human node, add a Loop node in ForEach mode over {{ matches.clusters }} so each approved cluster is handled in turn. Inside the loop body, add a Connector node using the MySQL connector in Direct mode with the update-rows tool. Point the table field at customers, set the columns you want to carry forward in set, and bind your where-clause values through params so they are escaped safely. For example, repoint child records or flag the duplicates as merged:
table: customers
set: { "merged_into": {{ cluster.keepId }}, "active": 0 }
where: id IN (?)
params: [ {{ cluster.duplicateIds }} ]
Flagging rows as merged (a soft delete) is safer than physically removing them, because it keeps an audit trail and lets you reverse a bad merge. If you prefer to fold data into the surviving row first, add another update-rows or execute-query step before this one. Finish with a Send Email node summarizing how many clusters were merged so the team has a record of each run.
Tips
- Filter on the AI confidence score before review: send only clusters where
{{ cluster.confidence }}is above a threshold (for example 0.85) into the Human node, and log lower-confidence matches for a separate manual pass. - Keep the read batch small (the
LIMITin Step 2) so each run stays well within time and credit budgets. You can always run the schedule more often instead of comparing the whole table at once. - Use the soft-delete pattern (a
merged_intocolumn) rather than deleting rows, so a mistaken merge can be undone without restoring a backup. - Ask Miraxa in the designer to scaffold the skeleton for you, for example "Add an Agent-mode MySQL Connector node with a Response Schema that returns duplicate clusters, then connect it to a Human node," and fine-tune the fields afterward in the properties panel.
Common Pitfalls
- Writing without approval. Never place the
update-rowsstep before the Human node. The Human node only continues on approval, so any merge step must sit downstream of it. - Unbounded prompts. Passing every column of thousands of rows into Agent mode inflates token usage and cost. Use the
jsonpickstep in Step 3 to send only the identity fields. - Unparameterized SQL. Always pass values through the
paramsarray ofexecute-queryandupdate-rowsrather than building query strings by hand, so special characters in names and emails do not break the query. - Timezone surprises. The Schedule trigger uses the IANA timezone you set, not the viewer's local time. Confirm the zone (for example
Australia/Sydney) so the run fires when you expect across daylight-saving changes.
Testing
Before scheduling it for real, validate on a tiny scope. Temporarily lower the Step 2 query to LIMIT 20 against a copy of the table, and seed it with a couple of obvious near-duplicates (same person, mistyped email). Use the Run button to execute once, then open the run in execution history and inspect the matches output to confirm the clusters look right. Approve the request from the Approvals inbox and verify the duplicates were flagged correctly in MySQL. Once the merges behave as expected, raise the limit and enable the Schedule trigger.