n8n Template · Salesforce · PostgreSQL · Data Sync
Free n8n workflow template that syncs Salesforce leads to PostgreSQL every 15 minutes with zero duplicates. Uses content hashing, ON CONFLICT upserts, and delta syncing via static timestamps.
A scheduled n8n workflow that syncs Salesforce leads to PostgreSQL every 15 minutes. Only processes changed records — not the full table. Uses MD5 content hashing to detect actual field changes, and PostgreSQL’s ON CONFLICT to make every write idempotent. Run it twice, get the same result.
Most Salesforce → Postgres sync setups fail the same way: they either re-insert everything on each run (duplicates) or skip records that genuinely changed (stale data). The naive fix — checking if a record exists before inserting — breaks under concurrency and still creates doubles if the workflow fires twice in a short window.
This template solves it with two mechanisms working together. First, delta syncing: n8n stores the last successful sync timestamp in workflow static data and passes it as a SystemModstamp > lastSync filter to Salesforce. Only genuinely new or updated records come through. Second, PostgreSQL’s ON CONFLICT (sf_id) DO UPDATE means the database itself enforces uniqueness — if the same record somehow arrives twice, the second write is a clean update, not a duplicate row.
The MD5 content hash is the third layer. If a record arrives but its key fields haven’t changed, the workflow skips the database write entirely. No unnecessary I/O.
Create this table before activating the workflow:
CREATE TABLE salesforce_leads (
id SERIAL PRIMARY KEY,
sf_id VARCHAR(18) UNIQUE NOT NULL,
first_name TEXT,
last_name TEXT,
email TEXT,
company TEXT,
phone TEXT,
status TEXT,
lead_source TEXT,
is_converted BOOLEAN DEFAULT FALSE,
owner_id VARCHAR(18),
sf_created_at TIMESTAMPTZ,
sf_updated_at TIMESTAMPTZ,
content_hash CHAR(32),
synced_at TIMESTAMPTZ
);
-- Index for fast lookups by sf_id (used on every sync check)
CREATE INDEX idx_sf_leads_sf_id ON salesforce_leads(sf_id);
-- Optional: index for querying by status or email downstream
CREATE INDEX idx_sf_leads_status ON salesforce_leads(status);
CREATE INDEX idx_sf_leads_email ON salesforce_leads(email); The UNIQUE NOT NULL constraint on sf_id is what makes ON CONFLICT work. Without it, the upsert node will throw on the first duplicate.
| # | Node | What it does |
|---|---|---|
| 1 | Schedule Trigger | Fires every 15 minutes. Adjust interval in node settings. |
| 2 | Get Last Sync Timestamp (Code) | Reads timestamp from workflow static data. First run defaults to 24 hours ago. |
| 3 | Fetch Updated Leads from Salesforce | SOQL filter: SystemModstamp > lastSync. Fetches all matching fields. |
| 4 | Any Records Found? (IF) | Routes to no-op branch if Salesforce returns empty. |
| 5 | Normalize Fields (Code) | Maps SF fields to snake_case. Lowercases email. Generates MD5 content hash. |
| 6 | Check Existing Record in Postgres | Queries by sf_id — returns existing content_hash if row exists. |
| 7 | Determine INSERT / UPDATE / SKIP (Code) | Compares hashes. Sets _action flag on each record. |
| 8 | Skip Unchanged Records (IF) | Routes SKIP records away from the DB write. |
| 9 | Upsert to PostgreSQL | INSERT ... ON CONFLICT (sf_id) DO UPDATE. Returns was_inserted boolean. |
| 10 | Update Sync Timestamp (Code) | Writes run start time back to static data. Logs insert/update counts. |
| 11 | Log Skipped (Code) | No-op for skipped records. Extend with Slack notification if needed. |
| 12 | No Records — Advance Timestamp (Code) | Still advances the timestamp so next run doesn’t re-scan the same window. |
.json file above.salesforce_leads table if it doesn’t exist.This template syncs the Lead object. To switch to Contacts or Opportunities, change the resource field in node 3 and update the field list to match. The rest of the workflow — hashing, upsert logic, timestamp management — is object-agnostic.
To sync multiple objects, duplicate the workflow once per object rather than trying to handle branching inside a single workflow. Easier to debug, easier to monitor independently.
15 minutes is a safe default for most teams. If you need near-realtime sync, consider switching the trigger to a Salesforce Outbound Message webhook instead of polling — this eliminates the schedule entirely and pushes changes from Salesforce the moment they happen. The rest of the workflow stays the same.
SystemModstamp only updates on field changes, not deletions. If you need to sync deletes, you’ll need to query Salesforce’s recycle bin separately or use the Bulk API with a full ID reconciliation pass.sync_log table for audit trailThe full writeup behind this template — including why most sync workflows produce duplicates and the exact query pattern that prevents them — is in Syncing Salesforce to PostgreSQL via n8n: The “No-Duplicates” Blueprint.
If your Salesforce API calls start hitting rate limits under load, the retry pattern from Bypassing “429 Too Many Requests”: Implementing Exponential Backoff applies directly to n8n HTTP Request nodes.