n8n MySQL Sync: Build a Two-Way Database Workflow
Build an n8n MySQL sync that upserts rows, dedupes, and runs both ways on a schedule. Exact node config, the upsert trick, and the parse steps tutorials skip.
A CRM holds the live contact record. MySQL holds the one the analytics team queries. Both drift apart within a day because nobody reconciles them by hand. n8n MySQL sync closes that gap: the workflow reads what changed, decides which side wins, and writes the difference, on a schedule you set once.
The tutorials that rank for this query describe the idea and stop. They show an Insert node and call it a sync. A real sync needs a dedupe step, an upsert so re-runs don't duplicate rows, and a way to handle the same record changing on both sides. This post covers those, with the exact MySQL node config and the SQL behavior that makes re-runs safe.
Syncing to a database is the part of automation people underestimate. The happy path is five nodes. The version that survives a second run is the one worth building.
What you can sync with the MySQL node
The MySQL node (n8n-nodes-base.mySql) runs Select, Insert, Update, Delete, and raw queries against any reachable MySQL or MariaDB instance. That covers the sync patterns most teams actually need:
- Mirror new CRM contacts into a reporting database the moment they're created
- Push form submissions into a MySQL table and back out to a Sheet
- Keep a product catalog in MySQL aligned with a Shopify or Airtable source
- Pull MySQL rows on a schedule and fan them out to Slack, email, or a dashboard
- Reconcile two systems nightly so analytics never queries stale data
- Archive old rows to a cold table and delete them from the hot one
The first three are sync jobs. The last three are read-and-route. Both lean on the same node; what differs is whether you write back.
The MySQL sync pipeline
Schedule Trigger (every 10 min)
→ Select changed rows (MySQL: WHERE updated_at > last_run)
→ Compare against target keys (Code / IF)
→ Upsert (MySQL Insert with ON DUPLICATE KEY UPDATE)
→ Log result + alert on failure
The load-bearing piece is the WHERE updated_at filter. Pulling the entire table every run and diffing in n8n works for a few hundred rows and falls over at a few hundred thousand. Let MySQL do the filtering. It's built for exactly that.
1. Read only what changed
Open with a Schedule Trigger (n8n-nodes-base.scheduleTrigger v1.2) and a MySQL Select that filters on a timestamp:
SELECT id, name, email, updated_at
FROM contacts
WHERE updated_at > '{{ $json.lastRun }}'
ORDER BY updated_at ASC
Store lastRun in n8n's static workflow data or a small state table so each execution picks up where the last left off. Pulling the full table every 10 minutes is the mistake that turns a tidy sync into a slow one.
2. Compare against the target
You've got the changed source rows. Now check what already exists on the target side. For small batches, an IF node (n8n-nodes-base.if v2) comparing a key field is enough. For larger sets, run a single MySQL Select that returns existing keys and match them in a Code node (n8n-nodes-base.code v2, jsCode) instead of querying once per row.
One query for 500 keys beats 500 queries for one key each. The execution log makes the difference obvious: a per-row loop shows 500 MySQL calls; the batched version shows one.
3. Upsert, don't blind-insert
This is what separates a sync from a duplicate factory. If your target table has a unique key on, say, email, the MySQL node's Insert operation can ride MySQL's ON DUPLICATE KEY UPDATE:
INSERT INTO contacts (email, name, updated_at)
VALUES ('{{ $json.email }}', '{{ $json.name }}', NOW())
ON DUPLICATE KEY UPDATE
name = VALUES(name),
updated_at = VALUES(updated_at)
Run that twice with the same email and the second run updates the existing row instead of inserting a copy. No unique key on the table? Then you must dedupe with the IF check in step 2 first, because a blind Insert will happily duplicate every row on every re-run.
4. Handle the two-way case
A genuine two-way sync needs both directions. One branch reads MySQL changes and writes them to the external app. A second reads the external app's changes and writes them back to MySQL. The tie-breaker when the same record changed on both sides is a last_modified timestamp: the newer write wins. The webhook-to-database workflow shows the inbound half of this, where an external app pushes changes in rather than n8n polling for them.
5. Log and alert on failure
A sync that fails silently is worse than no sync, because the data quietly diverges and you find out from a confused stakeholder. Wire an error branch that logs failed rows to Google Sheets and pings a channel. The Slack automation guide covers the alert side.
Implementation patterns
Pattern 1 — One-way mirror with upsert
Schedule Trigger
→ MySQL Select (WHERE updated_at > lastRun)
→ MySQL Insert (ON DUPLICATE KEY UPDATE)
→ Set lastRun = now
The simplest safe sync. The unique key plus the timestamp filter make re-runs idempotent. Run it a hundred times; the table stays clean.
Pattern 2 — Batched key comparison
MySQL Select (source changes)
→ MySQL Select (existing target keys, single query)
→ Code node (match in memory, split insert vs update)
→ MySQL Insert (new rows) + MySQL Update (changed rows)
This is the pattern for tables big enough that per-row queries hurt. The Code node does the set math; MySQL does the writes. In practice this is where most production syncs land.
Pattern 3 — Sheet-to-MySQL with validation
Google Sheets (read rows)
→ Code node (validate types, drop bad rows)
→ MySQL Insert (ON DUPLICATE KEY UPDATE)
Spreadsheets are full of dirty data: a phone number in the email column, a date typed as text. Validate before the write or MySQL rejects the batch and the whole run fails. The data validation workflow covers the cleanup step in detail.
n8n nodes you'll use most
| Node | Purpose |
|---|---|
n8n-nodes-base.mySql | Select, Insert, Update, Delete, and raw queries against MySQL/MariaDB |
n8n-nodes-base.scheduleTrigger v1.2 | Drive the polling interval for the sync |
n8n-nodes-base.if v2 | Gate writes and dedupe rows that already exist on the target |
n8n-nodes-base.code v2 | Batch-match keys in memory and validate row types before writing |
n8n-nodes-base.googleSheets | Source or sink for sync, plus a failure log for bad rows |
Don't point your sync at the root MySQL user. Create a dedicated account scoped to exactly what the workflow touches: read-only for reporting syncs, read-write on specific tables for the ones that write back. A leaked n8n credential then can't drop your database. This is cheap insurance and the n8n community forum logs plenty of cases where it would have helped.
Getting started
- Create a MySQL credential in n8n with host, port, database, user, and password. Use a scoped account, not root.
- Confirm your target table has a unique key on whatever field identifies a row. Without it, upsert can't work.
- Add a Schedule Trigger and a MySQL Select filtered on a timestamp column.
- Store
lastRunso each run reads only new changes. - Decide one-way or two-way, and wire the upsert with
ON DUPLICATE KEY UPDATE. - Add a Code node for batched key comparison if the table is large.
- Add an error branch that logs failures and alerts a channel.
For an intake pipeline that already handles validation and multi-system writes, the Data Entry Automation Hub is the closest fit.
Browse the data templates →The Data Entry Automation Hub ships the hard part of any sync — it collects submissions, validates and normalizes every field before it touches a system, then syncs the clean record to HubSpot CRM, a Trello board, and a master Google Sheet in one pass, with confirmation emails. Swap the Sheets sink for a MySQL Insert and you've got a validated database sync. It's part of The Complete n8n Templates Bundle, a one-time lifetime license to the whole catalog plus future templates, worth it if you run more than one data pipeline.
A MySQL sync is one of those workflows that looks done before it's safe. The upsert and the timestamp filter are what make it survive its second run and its hundredth. For adjacent patterns, the Google Sheets ETL workflow shows the extract-transform-load shape when a spreadsheet is the source, and the Airtable sync workflow covers two-way sync against a different backend. When you'd rather start from a working pipeline, the Smart To-Do List Manager demonstrates the same validate-then-write discipline against Trello and Sheets.
See all data automation templates →Common questions
How do I sync data to MySQL with n8n?
Can n8n do a two-way sync between MySQL and another app?
How do I avoid duplicate rows in an n8n MySQL sync?
How often should an n8n MySQL sync run?
Get the workflow templates this guide is built on
Import-ready n8n JSON, step-by-step setup, and tested end-to-end. One-time payment, own it forever.
Get 3 tested n8n templates, free
The full customer package for three real catalog templates — workflow JSON, step-by-step setup guide, credential checklist. Built through the same live-instance release process as everything we sell. Plus new templates and automation guides in your inbox. No spam, unsubscribe anytime.
- 01Smart To-Do List ManagerPre-built n8n workflow template that automates productivity with OpenAI. Live in about 10 minutes.$14
- 02Email Follow-Up AutomatorPre-built n8n workflow template that automates crm with OpenAI. Live in about 15 minutes.$12
- 03Market Trend AnalyzerPre-built n8n workflow template that automates data processing with OpenAI. Live in about 10 minutes.$14
More automation guides

n8n Trello Automation: Build Card Workflows That Actually Run
A new lead lands in a Google Form. Someone copies it into Trello by hand. Then again into the CRM. Then forgets the third place it was supposed to go. That manual relay is exactly the job n8n Trello a…

n8n Mailchimp Automation: Segment and Win Back Users
People sign up, poke around for a day, then go quiet. You can watch them sit in the database doing nothing. The list keeps growing, the engaged slice keeps shrinking, and the standard Mailchimp setup…

n8n Postgres Automation: Validate Data Before It Lands
A weekly report ran on partial data for three weeks before anyone noticed. No error, no alert. The numbers were just quietly wrong, and every decision made off them was wrong too. The fix wasn't a bet…