This workflow follows the Postgres → Slack recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "25-multi-db-bidirectional-sync-conflict-resolution",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "db-sync-trigger",
"responseMode": "lastNode",
"options": {}
},
"id": "e5f6g7h8-1111-4444-8888-000000000001",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
400
]
},
{
"parameters": {
"jsCode": "// \u0627\u0644\u06af\u0648\u0631\u06cc\u062a\u0645 \u062a\u0634\u062e\u06cc\u0635 \u062a\u063a\u06cc\u06cc\u0631 \u0648 \u0622\u0645\u0627\u062f\u0647\u200c\u0633\u0627\u0632\u06cc \u0645\u062a\u0627\u062f\u06cc\u062a\u0627\nconst item = $input.first().json;\nconst source = item.source_system;\nconst recordId = item.record_id;\nconst data = item.payload_data;\nconst incomingTimestamp = new Date(item.timestamp).getTime();\n\n// \u0627\u06cc\u062c\u0627\u062f Hash \u0633\u0627\u062f\u0647 \u0628\u0631\u0627\u06cc \u062a\u0634\u062e\u06cc\u0635 \u062a\u063a\u06cc\u06cc\u0631 \u0645\u062d\u062a\u0648\u0627\u06cc\u06cc\nconst dataString = JSON.stringify(data);\nlet hash = 0;\nfor (let i = 0; i < dataString.length; i++) {\n const char = dataString.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n}\n\nreturn [{\n json: {\n source_system: source,\n record_id: recordId,\n payload_data: data,\n incoming_timestamp: incomingTimestamp,\n data_hash: hash.toString(),\n sync_version: (item.sync_version || 0) + 1,\n is_loopback: item.is_loopback === true // \u067e\u0631\u0686\u0645 \u0636\u062f \u062d\u0644\u0642\u0647 (Anti-Loop)\n }\n}];"
},
"id": "e5f6g7h8-2222-4444-8888-000000000002",
"name": "Code: Hash & Metadata",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
400
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT id, data_hash, sync_version, last_updated, source_of_truth FROM central_patients WHERE id = '{{ $json.record_id }}';",
"options": {}
},
"id": "e5f6g7h8-3333-4444-8888-000000000003",
"name": "PostgreSQL: Get Existing",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
680,
400
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// \u0627\u0644\u06af\u0648\u0631\u06cc\u062a\u0645 \u067e\u06cc\u0634\u0631\u0641\u062a\u0647 \u062d\u0644 \u062a\u0639\u0627\u0631\u0636 (Conflict Resolution)\nconst incoming = $input.first().json;\nconst dbRecord = $input.first().json.postgresResult?.[0] || null;\n\n// 1. \u0645\u06a9\u0627\u0646\u06cc\u0632\u0645 \u0636\u062f \u062d\u0644\u0642\u0647 (Anti-Loop): \u0627\u06af\u0631 \u062a\u063a\u06cc\u06cc\u0631 \u062a\u0648\u0633\u0637 \u062e\u0648\u062f \u0633\u06cc\u0633\u062a\u0645 sync \u0627\u06cc\u062c\u0627\u062f \u0634\u062f\u0647\u060c \u0646\u0627\u062f\u06cc\u062f\u0647 \u0628\u06af\u06cc\u0631\nif (incoming.is_loopback) {\n return [{ json: { ...incoming, action: 'skip_loop', reason: 'Anti-loop triggered' } }];\n}\n\n// 2. \u0627\u06af\u0631 \u0631\u06a9\u0648\u0631\u062f \u062c\u062f\u06cc\u062f \u0627\u0633\u062a\nif (!dbRecord) {\n return [{ json: { ...incoming, action: 'full_sync', reason: 'New record' } }];\n}\n\n// 3. \u0627\u06af\u0631 \u0647\u0634 \u062f\u0627\u062f\u0647\u200c\u0647\u0627 \u06cc\u06a9\u0633\u0627\u0646 \u0627\u0633\u062a\u060c \u062a\u063a\u06cc\u06cc\u0631\u06cc \u0631\u062e \u0646\u062f\u0627\u062f\u0647\nif (dbRecord.data_hash === incoming.data_hash) {\n return [{ json: { ...incoming, action: 'skip_unchanged', reason: 'Data hash matches' } }];\n}\n\n// 4. \u062a\u0634\u062e\u06cc\u0635 \u062a\u0639\u0627\u0631\u0636 (Conflict Detection)\nconst dbTimestamp = new Date(dbRecord.last_updated).getTime();\nconst timeDiff = Math.abs(incoming.incoming_timestamp - dbTimestamp);\n\n// \u0627\u06af\u0631 \u062a\u0641\u0627\u0648\u062a \u0632\u0645\u0627\u0646\u06cc \u06a9\u0645\u062a\u0631 \u0627\u0632 5 \u062b\u0627\u0646\u06cc\u0647 \u0628\u0627\u0634\u062f\u060c \u062a\u0639\u0627\u0631\u0636 \u0647\u0645\u0632\u0645\u0627\u0646\u06cc (Race Condition) \u062f\u0631 \u0646\u0638\u0631 \u06af\u0631\u0641\u062a\u0647 \u0645\u06cc\u200c\u0634\u0648\u062f\nif (timeDiff < 5000) {\n // \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u062d\u0644 \u062a\u0639\u0627\u0631\u0636: \u0627\u0648\u0644\u0648\u06cc\u062a \u0645\u0646\u0628\u0639 (Source Priority: PostgreSQL > Airtable > MySQL)\n let winner = incoming.source_system;\n if (dbRecord.source_of_truth === 'postgresql') winner = 'postgresql';\n \n return [{ \n json: { \n ...incoming, \n db_record: dbRecord,\n action: 'conflict_manual_review', \n reason: `Race condition detected. Priority given to: ${winner}`,\n winner: winner\n } \n }];\n}\n\n// 5. \u0627\u0633\u062a\u0631\u0627\u062a\u0698\u06cc \u067e\u06cc\u0634\u200c\u0641\u0631\u0636: \u0622\u062e\u0631\u06cc\u0646 \u0646\u0648\u0634\u062a\u0627\u0631 \u0628\u0631\u0646\u062f\u0647 \u0645\u06cc\u200c\u0634\u0648\u062f (Last Write Wins)\nif (incoming.incoming_timestamp > dbTimestamp) {\n return [{ json: { ...incoming, db_record: dbRecord, action: 'propagate_update', reason: 'Last Write Wins' } }];\n} else {\n return [{ json: { ...incoming, db_record: dbRecord, action: 'skip_outdated', reason: 'Incoming data is older than DB' } }];\n}"
},
"id": "e5f6g7h8-4444-4444-8888-000000000004",
"name": "Code: Conflict Resolution",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
400
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.action }}",
"rightValue": "propagate_update",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c2",
"leftValue": "={{ $json.action }}",
"rightValue": "conflict_manual_review",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
}
}
]
},
"options": {
"fallbackOutput": "default"
}
},
"id": "e5f6g7h8-5555-4444-8888-000000000005",
"name": "Switch: Routing",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
1120,
400
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE central_patients SET payload_data = '{{ JSON.stringify($json.payload_data) }}', data_hash = '{{ $json.data_hash }}', sync_version = {{ $json.sync_version }}, last_updated = NOW(), source_of_truth = '{{ $json.source_system }}' WHERE id = '{{ $json.record_id }}';",
"options": {}
},
"id": "e5f6g7h8-6666-4444-8888-000000000006",
"name": "PostgreSQL: Update Central",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1340,
320
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE legacy_patients SET name = '{{ $json.payload_data.name }}', phone = '{{ $json.payload_data.phone }}', last_sync = NOW() WHERE id = '{{ $json.record_id }}';",
"options": {}
},
"id": "e5f6g7h8-7777-4444-8888-000000000007",
"name": "MySQL: Propagate to Legacy",
"type": "n8n-nodes-base.mysql",
"typeVersion": 2.2,
"position": [
1560,
320
],
"credentials": {
"mySql": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO sync_audit_log (record_id, source_system, action_taken, reason, timestamp) VALUES ('{{ $json.record_id }}', '{{ $json.source_system }}', '{{ $json.action }}', '{{ $json.reason }}', NOW());",
"options": {}
},
"id": "e5f6g7h8-8888-4444-8888-000000000008",
"name": "PostgreSQL: Audit Log",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1340,
480
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "C0000000000",
"mode": "list",
"cachedResultName": "db-sync-alerts"
},
"text": "\ud83d\udea8 *DATA CONFLICT DETECTED* \ud83d\udea8\n\n\ud83d\udcdd *Record ID:* {{ $json.record_id }}\n\ud83d\udd04 *Source:* {{ $json.source_system }}\n\u26a0\ufe0f *Reason:* {{ $json.reason }}\n\ud83c\udfc6 *Resolution:* Priority given to {{ $json.winner }}\n\n\ud83d\udd17 Please review the `sync_audit_log` table for manual intervention if needed.",
"otherOptions": {}
},
"id": "e5f6g7h8-9999-4444-8888-000000000009",
"name": "Slack: Conflict Alert",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.2,
"position": [
1560,
480
],
"credentials": {
"slackApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Code: Hash & Metadata",
"type": "main",
"index": 0
}
]
]
},
"Code: Hash & Metadata": {
"main": [
[
{
"node": "PostgreSQL: Get Existing",
"type": "main",
"index": 0
}
]
]
},
"PostgreSQL: Get Existing": {
"main": [
[
{
"node": "Code: Conflict Resolution",
"type": "main",
"index": 0
}
]
]
},
"Code: Conflict Resolution": {
"main": [
[
{
"node": "Switch: Routing",
"type": "main",
"index": 0
}
]
]
},
"Switch: Routing": {
"main": [
[
{
"node": "PostgreSQL: Update Central",
"type": "main",
"index": 0
}
],
[
{
"node": "PostgreSQL: Audit Log",
"type": "main",
"index": 0
}
],
[
{
"node": "PostgreSQL: Audit Log",
"type": "main",
"index": 0
}
]
]
},
"PostgreSQL: Update Central": {
"main": [
[
{
"node": "MySQL: Propagate to Legacy",
"type": "main",
"index": 0
}
]
]
},
"PostgreSQL: Audit Log": {
"main": [
[
{
"node": "Slack: Conflict Alert",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all"
},
"id": "25-multi-db-bidirectional-sync-conflict-resolution",
"tags": []
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
mySqlpostgresslackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
25-multi-db-bidirectional-sync-conflict-resolution. Uses postgres, mysql, slack. Webhook trigger; 9 nodes.
Source: https://github.com/kooroosh1363/agentic-automation-lab/blob/main/25-multi-db-bidirectional-sync-conflict-resolution/workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Messy Legacy Workflow. Uses acme-nodes-base, postgres, slack, emailSend. Webhook trigger; 10 nodes.
zaad9_teams_dane. Uses emailSend, airtable, slack, microsoftTeams. Webhook trigger; 7 nodes.
CMM. Uses httpRequest, postgres, redis. Webhook trigger; 90 nodes.
This workflow acts as a junior finance research analyst for a UK boutique M&A or corporate finance team. It listens for Slack messages, classifies the request, gathers company or market data, and prod
Scraping. Uses httpRequest, postgres, @apify/n8n-nodes-apify, respondToWebhook. Webhook trigger; 61 nodes.