Skip to main content

Example 2: Salesforce <-> MSSQL contact sync

Data Pipeline examples

Why this example​

The operations team owns Contacts in an internal MSSQL database; the sales team owns the same Contacts in Salesforce. Updates flow both ways: a customer-service rep updates a phone number in MSSQL, a sales rep updates the same Contact's address in Salesforce, and both records need to converge. A parent data pipeline calls two data pipelines with Sub-workflow nodes so they run in a known order: first Salesforce to MSSQL, then MSSQL to Salesforce, both using a LastModifiedDate watermark.

Helix Beverages context: this is the recipe that fixes CRM and ERP drift (named problem 1 in the Helix overview). Sales reps edit the same Contact in Salesforce that customer-success agents edit in MSSQL, and by Friday neither side trusts the other. A 15-minute reconcile keeps both stores converged without anyone having to pick a "winner" system.

At a glance​

  • Connectors used: Salesforce, MSSQL
  • Nodes used: Start, Source, Transform, Load, DbScript, Sub-workflow, Variable
  • Schedule: every 15 minutes, on the parent data pipeline
  • Direction: Two-way

Canvas​

Two sync data pipelines, called in order by a parent data pipeline.

Data Pipeline A (Salesforce -> MSSQL)

Data Pipeline B (MSSQL -> Salesforce)

Parent data pipeline (Contact sync)

The parent runs Data Pipeline A first, then Data Pipeline B, then sets the watermark. With the parent's On failure setting left at Stop the run, a failure in A stops the run before B starts. Schedule the parent as the Job.

Step-by-step​

Parent
1StartNoneNoneNone
2Sub-workflowData Pipeline APass parent rows: Ignore.None
3Sub-workflowData Pipeline BPass parent rows: Ignore.None
4VariableNoneRecord the run start time as lastSyncAt for the next run.None
Data Pipeline A
1StartNoneNoneNone
2DbScript: ensure stagingMSSQL DbScriptIF NOT EXISTS ... CREATE TABLE dbo.ContactsStg (...). Idempotent.None
3SourceSalesforce: query ContactSELECT Id, FirstName, LastName, Phone, Email, MailingAddress, LastModifiedDate FROM Contact WHERE LastModifiedDate > \{\{var.global.lastSyncAt\}\}SF Contact row
4TransformNoneRename Id -> salesforce_id, flatten MailingAddress.\{street, city, state\} -> three columns.MSSQL Contact row
5LoadMSSQL: MergeTarget dbo.Contacts, merge key salesforce_id.None
Data Pipeline B
1StartNoneNoneNone
2SourceMSSQL: querySELECT contact_id, first_name, last_name, phone, email, address_street, address_city, address_state, last_modified FROM dbo.Contacts WHERE last_modified > \{\{var.global.lastSyncAt\}\}MSSQL Contact row
3TransformNoneBuild SF Contact shape; include mssql_contact_id__c (an external-Id custom field) so the Load can upsert without dupes.SF Contact row
4LoadSalesforce: UpsertExternal Id field mssql_contact_id__c.None

Variations​

  • Conflict resolution: when both sides edit the same Contact between runs, the data pipeline that runs second wins. Add a Compute node before each Load that compares LastModifiedDate and skips the row when the destination is newer.
  • Three-way with HubSpot: add Data Pipeline C (HubSpot to MSSQL) and Data Pipeline D (MSSQL to HubSpot), and add a Sub-workflow node for each to the parent.
  • Bigger fields: if the row volume per run grows past a few thousand, switch the watermark from a single timestamp to a delta-table approach (Data Pipeline A appends to dbo.ContactsStg, Data Pipeline B reads from staging instead of from dbo.Contacts).