In the past, our salesforce.com teams have had to manually re-create records in Dynamics CRM, which robbed them of valuable time. To remedy this, we built an in-house solution to synchronize our CRM data into a single system of record, which we chose to be Dynamics CRM for a couple reasons. We have done some robust customizations to our internal system over the last few years to really make our process flow fit our business, and we also use it as the focal point of our internal project management and time tracking. Instead of rebuilding any of these pieces out again in salesforce.com, it made the most sense to leverage our existing work and push our salesforce.com projects into Dynamics CRM when we needed to start taking advantage of our other solutions.
Here are the steps we took to complete this sync:
1. Confirmed Business Requirements – The key objects/entities to sync were Accounts, Contacts, and Opportunities. We determined that we wanted this to occur automatically (versus with user intervention like a button) on create and edit of records.2. Mapped / Normalized fields – To start, we had to try to normalize the fields between Account, Opportunity, and Contact objects within salesforce.com and Dynamics CRM. While some of the fields overlapped, we had a number of custom fields that either had subsets of picklist values from the other system or fields that only existed in one system. The effort of creating a field mapping document usually proves to be a painful one, however we were able to cut down a lot of time by leveraging Metablast to generate our Dynamics CRM entity schema data instead of manually looking each entity up. Once we had that, we were able to compare it to the salesforce.com fields and finalize our desired mappings. Some specific Dynamics / salesforce.com gotchas are:
- Salesforce.com picklists are multi-select whereas Dynamics CRM are single select – we handled this by making the salesforce.com fields single select. Alternatively, we could have used N:N relationships on the Dynamics side.
- Salesforce.com leverages picklist values as having the label and value both being the same; Dynamics CRM allows you to have an integer value back a pretty label. Due to this, any picklist value coming from Salesforce.com needs to be converted into the respective integer representation from Dynamics CRM via code prior to attempting to save the new records in the target system.
{
"accounts": [
{
"name" : "Test Account",
"fax" : "555-434-8898",
"Salesforce_ID" : "001E000000JOAXj",
"contacts" : [
{
"firstName" : "Bob",
"lastName" : "Smith",
"Salesforce_ID" : "003G0000018UpQm"
},
{
"firstName" : "John",
"lastName" : "Doe",
"Salesforce_ID" : "003G0000017Bugo"
}
],
"opportunities" : [
{
"name" : "Test Opportunity",
"estimatedValue" : 10000,
"Salesforce_ID" : "006G000000LqXQI"
}
]
}
}
By nesting these records, it allows us to ignore API dependencies the first time through because we can create the Account, and then follow up with the Contact and Opportunity, all from a single request. It also allowed us to do some custom logic when there was a Contact lookup field on the Account, as we were able to do a second pass updating the Account field once the Contact was saved to the system. This approach also made it easier on the salesforce.com side because we did not have to worry about ensuring our Accounts all got created first, then sending over the remainder of our items. It also allowed us to limit the number of outbound HTTP requests we made.
5. Set-up Triggers – Once this endpoint was up and running, we set up triggers in salesforce.com on the three objects. Based on the business requirements mentioned above, we wanted this integration to be completely behind the scenes, and not affect the usability of the system at all for our salesforce.com users. This meant not slowing down their user experience when a synch was happening, or providing error messages to them about any sync failures.To alleviate the first issue, we decided to push the HTTP requests into future methods. This meant that our sync would happen asynchronously after the records had been saved in salesforce.com, allowing the user to save their record prior to the request being fired. These future calls usually happen within a minute or two (if not immediately), so the delay that we have in exchange for usability is trivial. For the second issue, in order to ensure that all sync errors would continue to be captured without notifying the user, we set up an email service to message an administrator with any errors that occurred. This allowed them to debug behind the scenes and then update the sync later.
6. Built API Request – After setting up the criteria in which we wanted to sync the records for each, we created Apex class objects to represent the JSON that our custom Dynamics CRM API was expecting. This allowed us to take advantage of JSON serialization that salesforce.com provides, allowing us to query for all the fields we need on a record, populate an instance of a custom Apex class object, and then serialize that object in a JSON string for our request body. A less complex example of a single account is shown below:
public class CRMAccount
{
public String name;
public String fax;
public String Salesforce_ID;
}
CRMAccount acct = new CRMAccount();
acct.name = "Test Account";
acct.fax = "555-434-8898";
acct.Salesforce_ID = "001E000000JOAXj";
String body = JSON.serialize(acct);
At this point, the body would look like the following, without needing to manually build the structure:
{
"name" : "Test Account",
"fax" : "555-434-8898",
"Salesforce_ID" : "001E000000JOAXj"
}
7. Handled API Responses – Once this request is built, we are able to send it to our API endpoint and receive a success or error response. Similar to building the request body, we can leverage JSON deserialization to turn the response body (also in JSON) into an Apex class object that we can easily reference, rather than having to manually parse it. Below is a simplified version of a single Account response:
{
"accountId" : "d8d27735-ba9d-433b-aa5a-dcb0bb2b4a1d",
"Salesforce_ID" : "001E000000JOAXj",
"errorMessage" : ""
}
And this would be the simple way to parse that into an object you can immediately reference:
public class CRMResponse
{
public String accountId;
public String Salesforce_ID;
public String errorMessage;
}
CRMResponse resp = (CRMResponse)JSON.deserialize(body, CRMResponse.class);
This simple response allows us to accomplish two of our goals:
- Upon successful record creation, save the Dynamics CRM ID into a custom salesforce.com field for reference in future field updates
- If the record fails to save/update, send the administrator an email referencing the ID of the record in both salesforce.com and Dynamics CRM, as a reference to the error generated by Dynamics CRM. Depending on how well the mappings were set up previously, this could be something overlooked like character limits being exceeded on a text field.
This particular integration was only a one-way sync, and to prevent changes to these records in Dynamics CRM we ensured that they could only be updated in salesforce.com by disabling those records in Dynamics CRM using Dynamic Forms. This allowed us to identify records with a Salesforce.com ID and differentiate them from Dynamics CRM created records.
If this was a bi-directional sync, we would have needed to implement the solution going the other way, and the logic behind it would have been very similar. Salesforce.com provides an out of the box REST API for record creation, but also allows developers to create custom web services, which would have allowed us to build out a very similar API to the one we built against Dynamics CRM.
Other caveats for companies interested in doing something similar:
- Validate with the users of both systems which fields are important, and ensure that you are mapping fields correctly as some might be interpreted incorrectly
- Ensure that you are persisting any relationships between records in the source into the target correctly
- If your source and target system have restrictive security, validate that your records are created with the right security model to ensure the wrong people don't get access to them
- If you are going to do a historical sync from the source to the target, ensure your workflows in the target system are disabled. Otherwise you might end up with a lot of email notifications that aren't current