Skip to main content
Engineering8 min readJanuary 10, 2026

Salesforce Integration Patterns: Lessons From Routiine.io

What I learned integrating Routiine.io with Salesforce — OAuth flows, data synchronization, conflict resolution, and why bi-directional sync is harder than it sounds.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Why Salesforce Integration Matters

Routiine.io is not trying to replace Salesforce. For organizations that have invested in Salesforce as their system of record, asking them to migrate to a new CRM is a non-starter. But those same organizations often find that Salesforce's reporting and intelligence capabilities do not surface the actionable insights their sales teams need — which deals are gaining momentum, which are stalling, and where the rep should focus today.

Routiine.io's value proposition for these customers is as an intelligence layer that sits alongside Salesforce. The CRM data stays in Salesforce. Routiine.io reads it, enriches it with momentum scoring, and surfaces insights that Salesforce does not provide natively. For this to work, the integration needs to be reliable, timely, and transparent.

The OAuth Dance

Salesforce integration starts with authentication. Salesforce uses OAuth 2.0 with the Web Server flow, which involves redirecting the user to Salesforce's authorization page, receiving an authorization code, exchanging it for access and refresh tokens, and storing those tokens securely.

The implementation is straightforward in theory and annoying in practice. Salesforce has multiple OAuth endpoints depending on whether the customer uses a production org, a sandbox, or a custom domain. The token exchange requires the correct endpoint for the customer's org type, and getting this wrong produces unhelpful error messages.

We handle this by asking the customer during the connection flow whether they are connecting a production or sandbox org, and by supporting custom domain entry for organizations that use MyDomain. The tokens are encrypted at rest and stored per-tenant, with automatic refresh handling when the access token expires. The refresh token itself has a configurable expiration in Salesforce admin settings, which means some customers' integrations stop working after a period of inactivity if their Salesforce admin has set an aggressive refresh token policy. We detect this condition and prompt the customer to re-authorize.

Data Synchronization Design

The synchronization between Routiine.io and Salesforce is bidirectional but asymmetric. Routiine.io reads most data from Salesforce — accounts, contacts, opportunities, activities, tasks. It writes back limited data — primarily custom fields for momentum scores and insight flags.

The read sync runs on a configurable schedule, typically every 15 minutes. Each sync cycle queries Salesforce for records modified since the last sync timestamp using the SOQL WHERE SystemModstamp > :lastSync pattern. This incremental approach keeps the data volume per sync manageable and limits API consumption against Salesforce's rate limits.

The sync process transforms Salesforce objects into Routiine.io's internal event and entity schemas. A Salesforce Opportunity maps to a Routiine.io Deal. A Salesforce Task of type "Call" maps to a Routiine.io communication event. These mappings are configurable per integration — different Salesforce orgs use different custom fields, different record types, and different picklist values. The mapping configuration UI lets the customer specify which Salesforce fields map to which Routiine.io concepts.

The write sync is more conservative. Routiine.io writes momentum scores and action recommendations back to Salesforce as custom fields on the Opportunity object. These writes happen after each momentum score recalculation, but only for scores that have changed. Writing unchanged scores would consume API calls without providing value.

Conflict Resolution

Bidirectional sync introduces the possibility of conflicts — what happens when a field is modified in both systems between sync cycles? This is a fundamental problem in distributed systems, and there is no universally correct solution.

Routiine.io's conflict resolution strategy is: Salesforce wins for shared data. If a deal's value is changed in both Salesforce and Routiine.io between syncs, the Salesforce value takes precedence. This is a deliberate choice based on the product's positioning — Salesforce is the system of record, and Routiine.io is the intelligence layer. The data authority should remain with the system that the organization has designated as authoritative.

For Routiine.io-specific data — momentum scores, insight flags, action recommendations — there is no conflict because Salesforce never modifies these values. The write direction is always from Routiine.io to Salesforce.

The conflict resolution policy is documented clearly in the integration setup and cannot be changed per-customer. We considered offering configurable conflict resolution but decided that the complexity of explaining and supporting multiple policies outweighed the flexibility benefit. Most customers agree with the "Salesforce wins" approach once the reasoning is explained.

Rate Limits and Resilience

Salesforce enforces API rate limits that vary by edition and license type. A typical Professional Edition org gets 100,000 API calls per 24-hour period. Enterprise Edition gets more. The limits are shared across all applications accessing the org, which means Routiine.io's integration competes for API budget with every other integration the customer has installed.

We manage this through a combination of efficient sync queries and adaptive scheduling. The incremental sync pattern minimizes the number of queries per cycle — typically one query per object type modified since the last sync. Bulk reads use Salesforce's composite API to batch multiple queries into a single API call.

If the integration approaches the rate limit, the sync frequency automatically decreases. Instead of every 15 minutes, it might back off to every 30 minutes or every hour. The customer is notified of the reduced frequency and can adjust by requesting a rate limit increase from Salesforce or by reducing other integrations' API consumption.

Error handling is designed for resilience. Individual record sync failures do not abort the entire sync cycle. If a specific opportunity fails to sync due to a validation rule or a permission issue, the error is logged, the record is marked for retry, and the sync continues with the remaining records. Persistent failures for specific records trigger an alert to the customer with the specific Salesforce error, since the fix is usually a Salesforce configuration issue rather than a Routiine.io bug.

The Salesforce integration is the most complex feature in Routiine.io, consuming more engineering time than any other single capability. But it is also the feature that unlocks the enterprise market segment, making it worth the investment both technically and commercially. The patterns we developed here — incremental sync, conflict resolution, rate limit management — are applicable to any system-of-record integration, which informed the architecture decisions for the platform as a whole.