Connectors / Service / Salesforce

Salesforce

Salesforce

Salesforce is a cloud based customer relationship management software suite.

Overview

Salesforce provides customer relationship management service software, and has a complementary suite of enterprise applications as well. These are focused on customer service, marketing automation, analytics, and application development. It is the market leader in CRM solutions.

Authentication

PLEASE NOTE: SFDC Editions with API Access include: Enterprise, Unlimited, Developer, and Performance. Any licenses that do not have this edition cannot communicate to external services via API.

When using the Salesforce connector, the first thing you will need to do is go to your Tray.io account page, and select the workflow you wish to work on. Once in the workflow dashboard itself, search and drag the Salesforce connector from the connectors panel (on the left hand side) onto your workflow.

With the new Salesforce connector step highlighted, in the properties panel on the right, click on 'New Authentication' which is located under the 'Settings' heading.

You will then need to select the 'scopes' for your authentication to configure exactly what access your Tray workflow will have to Salesforce:

Note that for standard use, you will always need to tick the Api scope.


In the next screen you will then be prompted to enter the credentials of the Salesforce user you will be authenticating with.

This user must be set up correctly in the Salesforce Admin UI, to ensure that you will be able to correctly access all of the available connector operations.

Correct setup of user permissions

Managing access

The Tray Salesforce connector gives you full access to all of your Salesforce data including leads, contacts, custom objects/fields, and even workflow rules and outbound messages.

Therefore you will need to make sure that the user you authenticate with has access to the correct objects and record types - in a read, write and create capacity, depending on need.

Managing permissions for users in Salesforce can be done using Profiles and Permission Sets.

NOTE: Currently the Salesforce connector can only be used on the Enterprise Edition of Salesforce, and higher. You may use the Salesforce connector on Professional Edition, but ONLY if you've requested API access from your Salesforce account manager. However, you will not be able to use instant WebHooks on the Professional Edition.

Managing access with Profiles

In Salesforce you can allocate a user to a pre-set profile, or you can create your own custom profile.

The available profiles depend on the type of License that a user has.

For example, a Salesforce Platform-licensed user can only have a Standard Platform User profile; while a Salesforce-licensed user (see Lisence Types for more details), can have multiple profiles including Contract Manager and System Administrator which gives comprehensive 'super user' access to all functions and objects.

The Contract Manager profile is more suited to the access level that is required by the Tray Salesforce trigger and connector. This can be seen by looking at the 'Standard Object Permissions' section of the Contract Manager profile settings:

Contract Manager Profile - Standard object permissions

You will note that the Standard Platform User does not have the same set of object permissions - for example it has access to Accounts but no access to Leads, Campaigns or Assets:

Standard Platform User - Standard object permissions

So a good approach would be to clone the Contract Manager profile, and then edit the permissions, bearing in mind the following:

In the Standard Object Permissions section you can set basic read and write access to objects such as contracts, leads and accounts.

In the Administrative Permissions section certain basic key permissions can be set - for example API Enabled must be ticked or the authentication will not work.

Several permissions in the General User Permissions section are relevant in that they allow extra functionality that cannot be granted purely through object-level permissions. For example Activate Contracts must be ticked in order for it to be possible to activate a contract, because the Edit permission on Contracts and Orders is not enough to allow contract activation (you can test activating contracts in Tray with the Update Record operation for the Contract record type - setting the Status field to 'Activated').

This applies to several other options in the General User Permissions section, such as Manage Cases and Activate Orders.

The Salesforce admin UI has tooltips which tell you what object settings are also required for certain permissions:

The following article also provides a useful reference for the object permissions which are required for certain General User Permissions, as well as the required editions: User profile permission descriptions.

For example 'Activate Contracts' links to the following page:

This tells you that you need to select Activate Contracts in General User Permissions and set Read and Edit rights on the contracts object in Standard Object Permissions - as already described above.

Permission Sets

In Salesforce it is also possible to use 'Permission Sets' to extend users' functional access without changing their profiles.

So you could create a Permission Set which has all the necessary access and then assign it to a particular user to make the necessary adjustments without affecting the profile they are assigned to.

When you create a Permission Set, you can specify the License that it is available for:

Salesforce Trigger

If you wish your workflow to be kicked off by a particular action in Salesforce, you can use the Salesforce Trigger.

The Salesforce trigger allows you to receive notifications and trigger workflows when given events occur associated with the selected trigger operation.

The Salesforce trigger is somewhat unique in that not only can you White-labelling the trigger (adjust objects with prefixes), but it also has two types of trigger events available:

  • Handling Events from the Salesforce Trigger (See immediately below) Followed by:
    • Standard trigger events
    • Manually-created trigger events

Important notes on Handling Events from the Salesforce Trigger

Handling events from the Salesforce Trigger has many aspects users may need to consider. As such it has been split into the following sub-sections:

Version 2.7+ vs 2.6 and earlier

From version 2.7 and above the Salesforce Trigger receives trigger events as an 'events' array, instead of an object which can be directly queried.

So any attempts to directly query an object with a jsonpath such as $.steps.trigger.Id will fail.

If you are creating a new Salesforce trigger, or updating your trigger from version 2.6 or earlier, you will need to set up a loop to go through the events array and take whatever action is needed for each item in the loop. Sometimes the trigger will return multiple events, but it can also return single events (which will then only trigger a single run of the loop).

The Loop Events step will need to set $.steps.trigger.events as the list to loop through. Then, as an example, a Get account Salesforce step could retrieve the account by pulling the id with $.steps.loop-1.value.Id:

White-labelling the trigger

The default behaviour of the trigger is to prefix the newly created objects with the string Tray.

From connector version 2.8 onwards, this prefix can be customised using the Object prefix field found in advanced settings.

The string provided will then be used as the prefix for objects created in Salesforce.

Standard trigger events

This type of trigger event is where Tray.io creates a number of Salesforce Objects which results in Salesforce sending notifications when a certain event occurs. These objects are the following:

  1. RemoteSiteSetting: Before any notification can be sent the target URL has to be registered as a RemoteSiteSetting.

  2. Workflow rule: A WorkflowRule is created to tell Salesforce when it should send an Outbound message to the workflow.

  3. Outbound message: An OutboundMessage is created and it defines what data is sent to the workflow.

The operations that fall under this category are:

Standard Trigger operations available

  • On custom workflow formula
  • On create
  • On create/update
  • On field change
  • On multiple field changes
  • On update

Full list of Salesforce operations can be found HERE

Manually Created trigger events

This type of trigger event is where you have to login into Salesforce and create/configure a number of Salesforce Objects which results in Salesforce sending notifications to your workflow.

The trigger events that fall under this category are:

Manually Created trigger operations:

  • On Apex Trigger
  • On Outbound Message

Full list of Salesforce operations can be found HERE

Webhook Setup

The Salesforce Trigger uses the same authentication method which is described in the first section of this page.

IMPORTANT! Note that when using the Salesforce trigger, you currently must use a permission set and add it to the User that you will be authenticating with. This permission set must have the 'Modify all Data' and 'Customize Application' permissions.

For more details on this operation please see our demo below called Trigger: Outbound message.

Salesforce Apex Trigger

Salesforce's Apex triggers allow you to trigger tray workflows in real-time, based on the events that occur in the Salesforce. Events in Salesforce could be anything like creating a new lead or changing the status of an Opportunity from "open" to "closed".

To configure the Apex trigger in your Salesforce, there are three files that should be created and configured with the appropriate code and saved into Salesforce.

The required three files are mentioned below. These files are generic, and you need to make changes to them depending on the object that your Salesforce trigger is tied to and the tray workflow for which you are configuring this Apex trigger. You can use editors like Sublime Text, Atom, or Notepad to make the required changes to these files.

To start with, login to your Salesforce account, and then you can make the specified changes to these files and save them as instructed into your Salesforce account:

Please note that the navigation instructions mentioned below to save the file in your Salesforce account correspond with the Salesforce's Classic view.

TrayWebhookTrigger.apex

// CHANGE: `YourObject` to the record type that you'd like to watch for changes.
trigger TrayWebhookTrigger on YourObject (before insert,before delete,after insert,after update,after undelete,after delete,before update)
{
if (TrayWebhook.firstRun) {
TrayWebhook.firstRun = false;
// CHANGE: replace this URL with the URL of your tray workflow, where you've added
// the Salesforce Notification trigger.
String url = 'https://c6670ba1-ad45-416e-8489-386c0456cbaa.trayapp.io';
String content = TrayWebhook.jsonContent(Trigger.new, Trigger.old);
TrayWebhook.callout(url, content);
}
}

Edit the above file as instructed:

  • Add the object type like Account or Opportunity that you'd like your Salesforce trigger to be tied to in place of YourObject on line 2 of this file.

    It should appear similar to this: trigger TrayWebhookTrigger on <YourObject> (before insert,before delete,after insert,after update,after undelete,after delete,before update).

    For understanding purposes, we are creating a workflow to trigger for the object type as Account: trigger TrayWebhookTrigger on Account (before insert,before delete,after insert,after update,after undelete,after delete,before update).

  • Copy and paste the Tray workflow's unique URL as a value to the 'String url' attribute. It should appear similar to this: String url = '<Tray workflow's URL>';.

    To get the unique URL of your Tray workflow, click the horizontal hamburger icon available on the top left corner of the Tray builder. Select 'Workflow settings' from the drop-down options.

    A 'Workflow settings' window will appear, copy the 'Workflow public URL' from this page and paste it into your code.

    After adding the URL, the code line should look like this: String url = 'https://c6670ba1-ad45-416e-8489-386c0456cbaa.trayapp.io';.

Once you are done making the above changes to the code, you can create a new file in Salesforce and save this code in it. To do so:

  • Click 'Setup' available on the top right corner of your Salesforce account and navigate to Build > Develop > Apex Classes from the left panel.

  • On the 'Apex Classes' page, click the 'New' button. This will open a code editor, copy and paste the above edited code to the code editor and click 'Save'.

TrayWebhook.apex

public class TrayWebhook {
public static boolean firstRun = true;
public static String jsonContent(List<Object> triggerNew, List<Object> triggerOld) {
String newObjects = '[]';
if (triggerNew != null) {
newObjects = JSON.serialize(triggerNew);
}
String oldObjects = '[]';
if (triggerOld != null) {
oldObjects = JSON.serialize(triggerOld);
}
String userId = JSON.serialize(UserInfo.getUserId());
String content = '{"new": ' + newObjects + ', "old": ' + oldObjects + ', "userId": ' + userId + '}';
return content;
}
@future(callout=true)
public static void callout(String url, String content) {
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(url);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(content);
h.send(req);
}
}

We need to make Similar changes to this file, as we have done to the above TrayWebhookTrigger.apex file.

  • Add the same object type you did for the above file, i.e., in this case, Account in place of YourObject on line 73 of this file. It should appear similar to this: SObject o = mock<('YourObject')>;.
  • Add the same Tray workflow's unique URL that you did for the above file on line 84 of this file. It should appear similar to this: System.assertEquals('<Tray workflow's URL>', request.getEndpoint());.

To save this file, follow the same navigation steps as above, and copy-paste the above code to the code editor, and click 'Save'.

TrayWebhookTriggerTest.apex

@isTest
public class TrayWebhookTriggerTest implements HttpCalloutMock {
private static HttpRequest request;
private static HttpResponse response;
public HTTPResponse respond(HTTPRequest req) {
request = req;
response = new HttpResponse();
response.setStatusCode(200);
return response;
}
static SObject mock(String sobjectName) {
SObjectType t = Schema.getGlobalDescribe().get(sobjectName);
SObject o = t.newSobject();
Map<String, Schema.SObjectField> m = t.getDescribe().fields.getMap();
for (String fieldName : m.keySet()) {
DescribeFieldResult f = m.get(fieldName).getDescribe();
if (!f.isNillable() && f.isCreateable() && !f.isDefaultedOnCreate()) {
if (f.getType() == DisplayType.Boolean) {
o.put(f.getName(), false);
}
else if (f.getType() == DisplayType.Currency) {
o.put(f.getName(), 0);
}
else if (f.getType() == DisplayType.Date) {
o.put(f.getName(), Date.today());
}
else if (f.getType() == DisplayType.DateTime) {
o.put(f.getName(), System.now());
}
else if (f.getType() == DisplayType.Double) {
o.put(f.getName(), 0.0);
}
else if (f.getType() == DisplayType.Email) {
o.put(f.getName(), 'foo@foo.com');
}
else if (f.getType() == DisplayType.Integer) {
o.put(f.getName(), 0);
}
else if (f.getType() == DisplayType.Percent) {
o.put(f.getName(), 0);
}
else if (f.getType() == DisplayType.Phone) {
o.put(f.getName(), '555-555-1212');
}
else if (f.getType() == DisplayType.String) {
o.put(f.getName(), 'TEST');
}
else if (f.getType() == DisplayType.TextArea) {
o.put(f.getName(), 'TEST');
}
else if (f.getType() == DisplayType.Time) {
o.put(f.getName(), System.now().time());
}
else if (f.getType() == DisplayType.URL) {
o.put(f.getName(), 'http://foo.com');
}
else if (f.getType() == DisplayType.PickList) {
o.put(f.getName(), f.getPicklistValues()[0].getValue());
}
}
}
return o;
}
@isTest static void testTrigger() {
Test.setMock(HttpCalloutMock.class, new TrayWebhookTriggerTest());
SObject o = mock('YourObject');
Test.startTest();
insert o;
update o;
delete o;
Test.stopTest();
System.assertEquals(200, response.getStatusCode());
// CHANGE: the URL below to the URL of your workflow
System.assertEquals('https://c6670ba1-ad45-416e-8489-386c0456cbaa.trayapp.io', request.getEndpoint());
if (request != null) {
Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(request.getBody());
System.assertNotEquals(null, jsonResponse.get('userId'));
}
}
}

There are no chnages to be made to the above code. To save this code into your Salesforce account from the same Setup page, from the left panel:

  • Select the 'Customize' option available under 'Build'.

    Select the type of the object you have specified in the TrayWebhookTrigger.apex and TriggerWebhookTriggerTest.apex files, i.e., 'Account' in this case, and after that, select 'Triggers'.

  • Click the 'New' button from the 'Account Triggers' page, copy and paste the above code to the code editor and click 'Save'.

Once you have created the above-mentioned three files in your Salesforce account, the last step is to add the unique workflow URL that you have added to the above files to the Remote Site.

To do so, from the left panel, navigate to Security Controls > Remote Site Settings. Click the 'New Remote Site' button.

On the Remote Site Edit page, add a suitable name to the site and the workflow URL in the 'Remote Site URL' field. Click 'Save'.

The final step is to test if our trigger works, to do so:

Create a workflow on the Tray platform with a Salesforce trigger and operation as 'On Apex trigger (advanced)'. Authenticate the trigger using the instructions specified in the Authentication section above.

Since we have configured an Apex trigger for the object type as 'Account'. The Salesforce trigger, will be fired once you make any changes to any of the existing account in Saleasforce or create a new Account.

On doing so the trigger will receive that specific account details. They would appear something similar to this:

There are various ways in which this data can be used and processed. We have assumed a simple scenario for understanding purposes, where we will add this data to a google sheet using a Google Sheet connector's 'Create row' operation.

If you make any changes to any of the fields in the Salesforce Account object, it would fire the above workflow and add the updates, i.e., the new values, to the google sheet. The updated google sheet would look like this:

Available Operations

The examples below show one or two of the available connector operations in use.

Please see the Full Operations Reference at the end of this page for details on all available operations for this connector.

Notes on using Salesforce

Data processing

When using Salesforce in production workflows, you will likely be dealing with large amounts of data - processing hundreds or thousands of rows.

In this case, you will likely need to make use of callable workflows to send data for sub-processing. Learn more about this in our course on callable workflows from the Tray Academy Live

You also may need to use our CSV Editor as an intermediate storage medium, before making use of the batch/bulk operations.

When dealing with large amounts of data in Salesforce it is important to be aware of:

  • What are the API limits according to your account, in terms of how many API calls can be made per day. Please the Salesforce API limits page for guidance on this
  • How can you use Tray.io to manage the amount of API calls that are being made?

In order to address the second point, you will see that certain operations for pulling data from Salesforce offer pagination options which allow you to grab batches of data in one and then loop through them with Tray.io, instead of making repeated single calls. The section below on Pulling data and pagination takes you through this in detail.

When pushing data to Salesforce you may need to make use of the batch create / update and bulk upsert operations. These allow you to reduce hundreds of record updates to a single call, and so are extremely important in terms of managing your API limits. Please the section on Pushing data and batch / bulk operations for detailed explanations of this.

Handling Leads in Salesforce

You can find a list of records from Salesforce, using the "Find Records" operation.

Once a lead is converted, you cannot update them in any way, and they are no longer available in the Salesforce interface.

Behind the scenes in the API, Salesforce uses a "Converted" field which is either true or false. You can use this in the Salesforce connector to get a list of leads who haven't been converted yet, for example.

Going a step further, leads can be converted into Contacts, Opportunities, and Accounts. In tray it's possible to get the "ID" of the relevant new converted objects using the "Converted Account ID", "Converted Opportunity ID" and "Converted Contact ID" fields.

Batch Operations

Batch pperations within Salesforce have many nuances depending on which operation is being used. Therefore this topic has been split into the following sub-sections:

Batch create list structure

As mentioned above, when using the 'Batch create' operation, the input for the Batch create list must be in the format of an array of arrays.

For example if your batch create list is set to pull a list of leads to create from data storage:

Then the input from debug would look like the following - if you were creating 2 new leads.

Note that each array is an array of key / value pairs:

{
"batch_create_list": [
[
{
"value": "Levicount",
"key": "LastName"
},
{
"value": "Strosin-Crooks",
"key": "Company"
},
{
"value": "Open - Not Contacted",
"key": "Status"
}
],
[
{
"value": "Francesconi",
"key": "LastName"
},
{
"value": "Wisozk-Cormier",
"key": "Company"
},
{
"value": "Open - Not Contacted",
"key": "Status"
}
]
],
"error_handling": "rollback_fail",
"object": "Lead"
}

Batch update list structure

As mentioned above, when using the 'Batch update' operation, the input for the Batch update list must be in the format of an array of objects.

For example if your batch create list is set to pull a list of accounts to update from data storage:

Then the input from debug would look like the following - if you were updating 3 accounts.

Note that each object contains an object_id on the same top level as a fields array of key / value pairs:

{
"batch_update_list": [
{
"fields": [
{
"value": "318-553-7145",
"key": "Phone"
},
{
"value": "Xinglong",
"key": "BillingCity"
}
],
"object_id": "0014J000007k2moQAA"
},
{
"fields": [
{
"value": "974-419-3972",
"key": "Phone"
},
{
"value": "Sveg",
"key": "BillingCity"
}
],
"object_id": "0014J000006m8EVQAY"
},
{
"fields": [
{
"value": "777-688-5363",
"key": "Phone"
},
{
"value": "Xingang",
"key": "BillingCity"
}
],
"object_id": "0014J000006m89GQAQ"
}
],
"object": "Lead"
}

Batch delete structure

When using the 'Batch delete' operation you only need to provide a simple array of IDs:

"batch_delete_list": [
"0014J000007k2moQAA",
"0014J000006m8EVQAY",
"0014J000006m89GQAQ"
]

Handling errors from batch updates

It is important to take certain steps to handle errors with the Batch Update operation which may pass undetected.

This is because, even though some error statuses might be returned by the Salesforce API, the fact that a response has been returned at all is treated as a successful execution by the Tray.io Salesforce connector.

For example a status of 404: NOT FOUND might be returned and, as you can see, in the debug output it has still returned green:

So you will need to set up a system for dealing with these errors. A very rudimentary approach would be a workflow which takes the following steps:

  1. Uses a boolean to check if the above output log returns hasErrors: true
  2. Sends a timestamp of the batch run to Slack
  3. Loops through the results from the Salesforce output
  4. Extracts all those whose status was not 204
  5. Sends a message to a batch errors channel in Slack for each account/lead error etc.

The end result would be notifications such as:

Another approach might be to use a script like the following which would dig back into the original update list you had compiled to replicate the info that was sent to Salesforce:

And produce a result such as:

PLEASE NOTE: Each object in the Batch Update array contains additional metadata, with field values nested inside a fields attribute.
// You can reference the input variables using input.NAME
exports.step = function(input) {
return _.map(input.rows, function(row) {
return {
"object_id": row.contact_id,
"fields": [
{
"key": "Role__c",
"value": row.role
}
]
}
});
};

Example usage

Salesforce and Trello

Below is an example of a way in which you could potentially use the Salesforce trigger and connector, to integrate with Trello. In this imagined scenario, upon creation of a Salesforce record, the workflow checks who created said card and if it was a Partner referral. Should this be the case, the Tray.io workflow will set a time limit for contact to be made.

The steps will be as follows:

  1. Setup the trigger and the first connector step in order to get the new record information.
  2. Create a boolean condition to base your output on.
  3. If a partner referral is not confirmed, create a Trello card as standard.
  4. If a partner referral is confirmed, set a contact due date on said Trello card.

The final outcome should look like this:

1 - Setup trigger & Get record info

Once you have clicked 'Create new workflow' on your main Tray.io dashboard (and named said new workflow), select the 'Salesforce Trigger' from the trigger options available:

Once you have been redirected to the Tray.io workflow dashboard, choose your Salesforce authentication, and set the operation to 'On Record Create'. You will see the option to select 'Record type' is mandatory. Choose from the dropdown options available: 'Lead'.

PLEASE NOTE: If you click on 'Show advanced properties' you will see the 'Object prefix' option mentioned earlier in the Trigger White-labelling the trigger section of this page.

Next, add the first Salesforce connector step. Search and drag over the connector from the left hand panel, into your workflow. Set the operation to 'Find records' and 'Record type' to 'Lead'.

While this step will seem similar to the first one, its purpose is different. The trigger is activated upon a new Lead record being created. The connector step finds said record to get the record info.

Below this section is an input field that is mandatory - 'Conditions matching'. As this is pre-filled to 'Match all conditions' you need not worry about it for this example.

2 - Set the conditions

Add another connector step to your workflow, this time utilising the Boolean core connector.

It is here that the conditions are set regarding the two alternative outputs we intend to make.

The first value is generated using jsonpaths and the connector-snake.

JSONPATHS: For more information on what jsonpaths are and how to use jsonpaths with Tray.io, please see our Intro page and Data Guide for more details.
CONNECTOR-SNAKE: The simplest and easiest way to generate your jsonpaths is to use our feature called the Connector-snake. Please see the main page for more details.

The first input will be: $.steps.salesforce-1.records[0].LeadSource. This means that the the LeadSource found in the step previous will be what is tested.

The 'Comparison type' is set to: Equal to which is self explanatory. The final section is what the LeadSource is queried against: Partner Referral.

PLEASE NOTE: Make sure to double check your input selectors are set accurately! The first condition is set as a jsonpath, while the next two are strings. If this is not correct then your workflow will come back with an error.

3 - False branch

On the left hand branch of the boolean condition, aka the 'FALSE' branch, drop and drag a Trello connector.

Add your Trello authentication as appropriate, and set the operation to 'Create new card'.

PLEASE NOTE: If you don't have your Trello authentication set up, please see our Trello docs for more details.

Your 'Board ID' will be available from a list of dropdown options, as will your 'List ID'. The 'Name' field we can generate via utilising the connector-snake once more and linking it to "Salesforce step 1" again as it has all the information regarding said record (including its name).

In the description field our demo is using Tray.io's own interpolation method to auto-generate the output so that it can be displayed easily.

INTERPOLATION: When you wish to include JSON generated data within another input/ output/ result, use our Interpolation method as described here.

Your connector should look similar to this be the end of setup:

Notice that the 'Due Date' is left blank here, as we have not set it (check the bottom right hand corner of the above image). While this is not a mandatory field, it will be important on the 'TRUE' branch later.

4 - True branch

On the right hand branch of the boolean condition, aka the 'TRUE' branch, drop and drag a Date & Time Helper connector.

This connector will be used to get the current time stamp for the workflow being processed. Set the operation to do just that by selecting 'Get current timestamp'. choose your timezone, and your preferred format as well.

Take a second Date & Time Helper connector and place it below the first one. In this case the operation will be creating a set time: 'Add to date'. The only other field we need fill in is the 'business days' option, which in this demo will be 1 day.

Finally add a Trello step and fill it in as described previously. This time you may wish to highlight the fact that this was a partner referral within the card description.

Using the connector-snake once more, fill in the due date the time helpers step 2 result: $.steps.date-time-helpers-2.result

Your final step should be filled out as follows - you will notice that we have also included the due date in the description step as well:

Once you run your workflow, depending on your outcome you will now auto-generate Trello cards, both will have descriptions attached as per our interpolation within the descriptions we made earlier.

Below is an example of one card that is in viewing mode, which has a partner referral and therefore also includes a due date:

Congratulations! your Salesforce workflow is now complete.

Trigger demos

Here are two workflows that show how to work in conjunction with the Salesforce trigger, specifically when using the 'On custom workflow' and 'On Outbound message event' trigger operations:

Custom workflow

Below is an example of a way in which you could potentially use the Salesforce trigger using the 'On custom workflow' operation. Due to the complexity, we have very briefly outlined the method and included links to the Salesforce documentation where necessary for further guidance.

The steps will be as follows:

  1. Select and set up the Salesforce trigger.
  2. Create a Salesforce workflow formula.

The final outcome should look like this:

1 - Setup trigger

Once you have clicked 'Create new workflow' on your main Tray.io dashboard (and named said new workflow), select the Salesforce trigger from the trigger options available:

Once you have done this, make sure to either choose a previous Salesforce authentication or create a new one as appropriate.

Set the operation to 'On custom workflow formula':

Please note that the 'Record type' field is mandatory, and that for this example we have set it to 'Contact'.

2 - Create Salesforce workflow formula

The second mandatory field is 'Formula'.

This is to let Salesforce know when they need to send a notification to your workflow.

The formula in the example below tells Salesforce to send a notification when a new account is created or when the field 'Name' is updated in an account: ISNEW()||ISCHANGED(Name)

For more information about how to create your own formulas, check out the following Salesforce documentation links:

Outbound message

Below is an example of a way in which you could potentially use the Salesforce trigger, with the 'On Outbound message event' operation.

Due to the complexity of the Salesforce UI, we have outlined the navigation as clearly as possible for the sake of our users.

The steps will be as follows:

  1. Select and set up the Salesforce trigger.
  2. Get your Tray.io public URL.
  3. Update your 'Remote site settings' within Salesforce.
  4. Create and set up your new Workflow Rule.

The final outcome should look like this:

1 - Setup trigger

Once you have clicked 'Create new workflow' on your main Tray.io dashboard (and named said new workflow), select the Salesforce trigger from the trigger options available:

Once you have done this, make sure to either choose a previous Salesforce authentication or create a new one as appropriate.

Set the operation to 'On outbound message':

Please note that the 'Record type' field is mandatory, and that for this example we have set it to 'Account'.

2 - Get the Public URL

The first thing you will need is your Tray.io public URL. This can be found in the settings of your workflow dashboard.

Go to the top left hand setting cog.

Once you click through, you will be redirected to the main workflow settings page. Your 'Workflow Public URL' will be displayed clearly. Copy this URL, and save it for now for use later.

3 - Updating 'Remote site settings' in Salesforce

First login into your Salesforce account.

Click on the setting cog in the top right hand corner, near your profile picture. In the dropdown options displayed, select 'Setup'.

This will direct you to the Salesforce main Setup page. In the search bar at the top, query the term: Remote Site Settings

This can also be found by going to the the Setup right hand navigation menu, selecting Settings -> Security -> Remote Site Settings.

You will be redirected to a new tab, that should list all your remote sites available. In the centre of this table at the top, you will see a 'New Remote Site' button.

Click on the 'New Remote Site' button, and configure the new remote site settings where the field: 'Remote site URL' is the value of the workflow public URL, that you copied earlier from the Tray.io dashboard settings.

When finished hit save.

4 - Create your Workflow Rule

Go once more the the Salesforce search bar and type in Workflow Rules. You will be redirected to a new Salesforce section which should have the title of "ALl Workflow Rules". In the middle of the layout, there is a button much like before, which gives you the option to create a new value (in this case, 'New Rule').

Click on this button to be redirected once more, this time to the 'New Rule' setup page. This is where we need to configure the new workflow rule. This rule, is what will be used to tell Salesforce under what specific circumstances, you want your Salesforce Tray.io workflow to be be triggered (or rather, send your workflow a notification).

In this case, the workflow rule will apply to the 'Account' object. Select this from the dropdown options available.

The workflow itself will be triggered only when a an account whose name starts with the letter R is created.

Fill in the evaluation and rule criteria as shown using the dropdown options etc:

When you click through to the next page, you will need to say what happens when the event (that was just defined) has occurred. In this case, we want to create a new outbound message:

As soon as you select 'New Outbound Message' you will be redirected to another screen. This is where you will dictate WHERE and WHAT information you will be sending.

IMPORTANT!:In the Endpoint URL field, make sure to once more add the Tray.io public URL that you took note of earlier.

You can also then say what information you want sent in the workflow notification. The example below is sending the new Accounts Id and Account Name.

Once you have set this up and clicked through you will come to the final page which is in fact the "Workflow Rules" configuration page from earlier. Press the 'Done' button to complete setup.

All that is left is you for to activate your Tray.io workflow and wait for an account name (with the starting letter of "R") to be created and your workflow will run.

Advanced topics

Managing API limits

Managing API limits within Salesforce has many aspects users may need to consider and so has been split into the following sub-sections:

Please take care to read the Important notes on Batch Operations section when dealing with any batch related topics. It too has several topics users need to be aware of before progressing.

Pulling data and pagination

When using certain operations such as the 'Find Records' operation, you will find there are 'pagination' options which allow you to pull results from Salesforce in batches.

This means you can limit the number of API calls you make - e.g. you can retrieve 10 batches of 200 records which match your criteria, instead of making 2000 individual calls for each record (the size limit for each batch is actually 2000, so you could make this in one call).

The following workflow shows a basic pagination system where we are pulling batches of leads from Salesforce with a rating of 'warm' in order to loop through them and send them to another third-party service:

  1. The main Loop forever loop uses the 'Loop forever' operation, which we will set to stop when all batches have been processed
  2. The Get offset Data Storage step uses the 'Get value' operation to get the 'next_page_offset' token from the last batch of leads (on first run this will be null)
  3. The Get warm leads Salesforce step (see screenshot below) uses the 'Find Records' operation to set the leads filter to only include those with a 'Warm' rating, set the batch size (e.g. 200) and input the 'next_page_offset' token retrieved from the first Data Storage step ($.steps.storage-1.value) (note the Salesforce API allows this to be null on the first run)
  4. Call batch workflow is a Call Workflow step which gets the leads batch using $.steps.salesforce-1.records and calls another workflow to process the leads in batches (this is a best practice in terms of efficiency (callable workflows run in parallel and so don't wait until each batch is finished!) and simplifying the layout of your main workflow)
  5. The Last batch? boolean step checks to see if $.steps.salesforce-1.next_page_offset is equal to null, in which case the last batch has been processed and we can break the loop (make sure the main 'loop forever' loop is the one that is set to break otherwise it will loop forever!)
  6. The Set offset Data Storage step uses the Set value operation to set $.steps.salesforce-1.next_page_offset as the offset which can then be retrieved by Get offset at the start of the next batch

The following screenshots show how the Get warm leads step is set, including the pagination parameters of batch size, and pulling in the offset token from the Get offset Data Storage step:

And this shows how the Call batch workflow step passes the leads as Data to the callable workflow:

The following shows a very simple implementation of the batch processing callable workflow, which loops through each lead of the current batch and adds them to Google Sheets using the 'Create row' operation:

The Loop through leads step uses $.steps.trigger.leads to loop through each lead of the current batch and add them to Google Sheets using the 'Create row' operation

IMPORTANT!: Note that in this scenario you will likely also be subject to the API limits of the service you are pushing records to (Google Sheets in this example). In which case you would likely need to set up a CSV queue as described in CSV queue management
PLEASE NOTE: Note that Google Sheets is acting here as a generic placeholder for any third-party service you may wish to use! For example you may be using Salesforce in conjunction with Google Ads, as discussed in our blog post How to improve ROAS and automatically exclude undesired leads.

Pushing data and batch / bulk operations

As mentioned above, when you are pushing large batches of data to Salesforce, you will need to control the rate at which you do so in order not to exceed your API limits.

  • 'Bulk upsert' is effectively two operations in one - if a record of a particular type (e.g. 'lead') is found then it will be updated, if it is not found then a new one of that type (e.g. 'lead') will be created
  • 'Batch Update' / 'Batch Create' can be used together when, in the case of a record of a particular type (e.g. 'account') not being found, you wish to create a record of a different type (e.g. 'lead')

Bulk upsert

PLEASE NOTE: The 'Bulk upsert' operation only accepts data in CSV format

This demo imagines a scenario whereby, for a particular Salesforce Account, you have pulled together information on a number of the Salesforce contacts for that Account (i.e. the people who work for that company) - from a number of sources (e.g. enrichment info from Zoominfo and Clearbit) and you are wanting to achieve the following:

  • upload this contact information to Salesforce using a limited number of API calls
  • if a contact doesn't exist in Salesforce, then the upsert operation will auto-create a new contact using the information provided

The following screenshot shows that we are picking the workflow up from the point where we have pulled the contact data together into data storage.

We then take the following basic steps:

  1. Fetch the records and count the total number
  2. If the total number of records found is only 1, use the single 'Create/update record' operation
  3. If the total number of records found is more than 1, create a CSV to store the contacts in (this is because data must be in CSV format for the Bulk upsert operation)
  4. Export the CSV file and upload the data to Salesforce using 'Bulk upsert'

In detail the steps being followed are:

  1. Get Contact Data is a Data Storage step which uses the 'Get value' operation to retrieve the stored list of Contact Data

  2. The Get Contact Count List Helpers step uses the 'Count items' operation to count the number of contacts returned by $.steps.storage-4.value

  3. The Contact Found? boolean step checks that $.steps.list-helpers-3.result is greater than 0

  4. If so then the More than 1? boolean step checks that $.steps.list-helpers-3.result is greater than 1

  5. If there is only one contact then Upsert/Link Contact uses the single 'Create / update record' operation:

This pulls the account id from the workflow trigger (a created account is the trigger for this workflow) and the name, phone, email details etc. from data storage

  1. If there is more than one then we will have to use the CSV Editor to Create CSV for bulk upsert:

Note that here we are only initiating the CSV with the required columns (FirstName, LastName, AccountId etc.)

IMPORTANT!:The names of the CSV columns must exactly match the Salesforce schema - i.e. you must use'FirstName' (not e.g. 'first_name'), 'Phone' (not e.g. 'phone' or 'landline'), 'AccountId' (note.g. 'account_id') etc.
  1. Loop Contacts then loops through the results from data storage:
  1. For each contact we then Add CSV row:

Here we have to pick up the CSV ID from the previous create CSV step

And we use the values from the loop connector to add the contact details

This loop will run until all contacts have been added to the CSV

  1. We then have to Export CSV to make the complete csv file available for upsert:
  1. The final step is to Bulk Upsert/Link Contacts with the 'Bulk upsert' operation:

Note that 'External field name' must be used to specify what is the field which will be used to identify the contact

IMPORTANT!: In implementing a solution like the above you would need to make sure that you do not overload data storage (the limit for data stored under a single key is 400K). So if you are processing thousands of records you would likely use a Callable Workflow to send them off for sub processing.

Polling a bulk upsert

When a bulk operation is used, Salesforce does not process the data immediately, instead it starts a bulk data load job. The time in which it takes for this job to finish depends on resources available in your Salesforce instance.

When you use a bulk data operation it receives a Job ID (or just ID as shown in the connector step output).

This job ID can then be used to poll for the status of the job. Only when the job shows a status of JobComplete has the data been successfully processed in Salesforce.

The following workflow shows a Bulk Upsert job has been started - pulling a CSV file from a trigger:

The Repeat polling call step uses the 'Loop Forever' operation.

The Poll SF - check job status step uses the 'Get job info' operation. It pulls in the 'Job ID' using the $.steps.salesforce-1.id jsonpath

On each iteration of the Loop, we check if the job has succeeded by looking at the status field. If the job has completed it will show a status of JobComplete:

Is job complete? is a boolean step which checks if $.steps.salesforce-2.state is equal to JobComplete.

IMPORTANT!: As per Salesforce's API docs the completed status could also be UploadComplete. The following screenshot from the Salesforce docs shows the different job statuses you might check for:

The TRUE branch of the boolean contains the Break loop step (referring to the correct loop! 'loop-1' in this case).

The FALSE branch of the boolean has the Delay and repeat loop step:

You can set the delay to be e.g. 1 minute before the next check.

A key point here is that each check is an API call, so if you are running a large update job you don't want to be checking every 10 seconds!

IMPORTANT!: When using the 'Loop forever' operation to poll for a status you should factor in the possibility that a status will never be reached. To allow for this you should include a check on how long the loop has been running, as illustrated in our Loop Connector documentation.

Get Job Information

Add a final Salesforce connector step to your workflow. This is used to gather the job information (how many file uploads, which failed, time taken etc). Set the operation to 'Get job info' and the 'Job ID' to: $.steps.salesforce-1.id.

In your Debug panel you should see results similar to below:

Batch update / batch create

As mentioned above, there may be a bulk upload scenario where, if a record is not found then you want to create a record of a different type (which is not possible with 'Bulk upsert').

This section will be a very simple demo of using a combination of the 'Batch update records' and 'Batch create records' operations to achieve this.

IMPORTANT!: The Salesforce API will only receive batch update / create lists in an exact format. Please see the below Important notes on batch operations for details of this and ways to handle errors returned by Salesforce.
IMPORTANT!: This demo is only to show how the correct object and list structures can be created using Tray core and helper connectors. When processing batches of data using Data Storage lists, you need to be conscious of the fact that the storage limit under one key is 400K. One way to handle this, as per this demo, is for batches to be sent for parallel processing to a callable workflow which uses 'current run' data storage that is cleared after each run. Another approach might be to use the CSV Editor.

The scenario is that a callable workflow is receiving batches of data such as the following:

The idea is that all records with an object_id are pre-existing Salesforce Accounts that need to be updated with Phone and BillingCity.

While records without an object_id do not yet exist in Salesforce and we need to turn them into Leads which might become Accounts at a later stage.

The screenshot below shows a callable workflow which is receiving these batches of records:

The data being received by the Callable Trigger is in json format:

The following steps are taken through the course of the workflow:

  1. Loop through batch uses $.steps.trigger.records to loop through the records received by the trigger

  2. The boolean step Contains SF id? checks if object_id exists:

  1. Depending on the result the True and False paths then add the current record to either a 'create-list' or an 'update-list'

A key point here is that we must make sure the record objects being created adhere to the key/value pair structure required by Salesforce

Depending on how complex the data you are receiving is, you may need to use a script connector to process your data, as discussed in processing batch updates

For the purposes of this demo, however, we can achieve this with an Object Helpers step, using either 'Enforce Object Structure':

Or 'JSON parse':

For a 'create' object the structure is:

And for an 'update' object the structure is:

The data storage Append to create-list and Append to update-list steps use the 'Append to list' operation:

  1. The subsequent Get create-list and Get update-list data storage steps then use Get value to retrieve these lists
  1. Both the Leads to create? and Accounts to update boolean steps check if these lists are returning empty arrays

  2. The Batch Create operation then uses the resulting 'create-list' from data storage:

Crucially, the structure of the input list conforms exactly to what is required by Salesforce:

And the Batch Update operation uses the resulting 'update-list' from data storage:

Again, the structure of the input list conforms exactly to what is required by Salesforce:

Deduplicating and merging Salesforce Records

Over time you may find that duplicate records build up in your Salesforce database.

The following workflow imagines a scenario whereby new leads being created need to be checked for pre-existing duplicates:

  1. The Listen for Lead Creation triggers the workflow when a new lead is created in Salesforce
  2. Get New Lead uses the 'Find records' operation, using the $.steps.trigger.events[0].Id jsonpath to retrieve the newly created lead, including all the relevant associated fields (FirstName, LastName, Email, Phone, Lead ID, Company etc.)
  3. We then conduct a series of 3 checks to see if this lead already exists as a duplicate (explained in detail below)
  4. If any of these checks find a duplicate, the Salesforce id for the duplicate lead is stored using a Data Storage set id step ('Set Value operation'), and the workflow moves on to the de-duplicating stage
  5. If all 3 checks fail to find a duplicate then the workflow is terminated, as no action needs to be taken
  6. If deduplicating is required, the Data Storage get id step uses 'Get Value' to retrieve the id of the duplicate lead
  7. Get Old Record is a 'Find Records' operation which uses $.steps.storage-3.value to retrieve the duplicate lead, including all the relevant associated fields (FirstName, LastName, Email, Phone, Lead ID, Company etc.)
  8. Build Merged Lead Values is a script which then replaces all the old duplicate values for name, email, etc. with the values from the new lead. If they are not present for the new lead then the original old values will be used
  9. Merge Leads finally uses the Salesforce 'Merge Records' operation with the new Lead ID ($.steps.trigger.events[0].Id) as the Master record ID and the old Lead ID ($.steps.storage-3.value) as the 'Record to merge'. The 'Fields to populate' come from the result of the merged values script ($.steps.script-1.result)
PLEASE NOTE: You can import the above workflow yourself for inspection and testing. Just click here to download the workflow file. To import just create a ne workflow with any kind of trigger, then click on the import option in the top-left of the workflow builder:

The duplicate checks explained

When making the above duplicate checks, we are making a series of checks of varying degrees of certainty.

The first check is by email:

Looking at the properties panel, we can see how this check is set up:

You can see that while a check is being made to see if any existing leads match this new lead by email, we are also checking for First Name, Last Name and Vertical (i.e. automotive, banking, consumer etc.)

The subsequent checks by phone and company use the same conditional setup, replacing email with phone and company.

By the end of this process we will have checked with approx 99% degree of certainty for pre-existing duplicates (these aren't absolute failsafes as there could be a typo in each field we are checking)

Merging the new and duplicate record

After using the Data Storage get id step to retrieve the id of the identified duplicate, we fetch the duplicate lead itself:

The Build Merged Lead Values script step then pulls in the old Lead values and new Lead values as variables:

The script itself is simple:

// You can reference the input variables using input.NAME
// Parsed JSON files could be referenced as fileInput
exports.step = function(input, fileInput) {
let values = [];
for(let key in input.newLead){
if(key !== "Id"){
if(input.newLead[key] === null){
values.push({
"name": key,
"value": input.oldLead[key]
});
} else {
values.push({
"name": key,
"value": input.newLead[key]
});
}
}
}
return values;

It will replace all values from the old lead with values from the new lead.

If the new lead does not have a particular value, then the old lead value will be retained.

The output is then in a format which will be accepted by the Salesforce schema for merging records. For example:

{
"result": [
{
"name": "FirstName",
"value": "Roger"
},
{
"name": "LastName",
"value": "Ramjet"
},
{
"name": "Phone",
"value": "(850) 777-2436"
},
{
"name": "Id",
"value": "00Q1QxxxxxxYGUA3"
},
{
"name": "Vertical__c",
"value": "Automotive"
},
{
"name": "Company",
"value": "Ramjet and co"
},
{
"name": "Email",
"value": "info@ramjet.net"
},
{
"name": "MobilePhone",
"value": "(850) 755-3555"
}
],
"console": []
}

The output of the script step can then be used in Fields to populate for the Merge leads step:

SOQL Query & SOSL Query operations

The Salesforce Object Query Language (SOQL) and Salesforce Object Search Language (SOSL) APIs can be used to search your organisation’s Salesforce data.

Generally speaking when using these query operations, you should use SOQL to query one operation at a time, and SOSL when you want to search text, email, phone fields, etc (basically when searching multiple objects simultaneously). This is why the latter is better to use when querying a relationship between objects and such.

Here are some points you need to consider when choosing between the two query operations:

Use SOQL when you know which objects the data resides in, and you want to:

  • Retrieve data from a single object or from multiple objects that are related to one another.
  • Count the number of records that meet specified criteria.
  • Sort results as part of the query.
  • Retrieve data from number, date, or checkbox fields.
IMPORTANT!: Please note that for for SOQL Query to accept your date references, the format MUST BE: YYYY-MM-DD. Please see the SOQL API Documentation site for more details.

Use SOSL when you don’t know which object or field the data resides in, and you want to:

  • Retrieve data for a specific term that you know exists within a field. Because SOSL can tokenize multiple terms within a field and build a search index from this, SOSL searches are faster and can return more relevant results.
  • Retrieve multiple objects and fields efficiently where the objects might or might not be related to one another.
  • Retrieve data for a particular division in an organization using the divisions feature.
  • Retrieve data that’s in Chinese, Japanese, Korean, or Thai. Morphological tokenization for CJKT terms helps ensure accurate results.

The language used is crucial for your search patterns. In principal, it is extremely similar to SQL in text style.

With the above taken into consideration, below is an example of a SOQL query, which also involves using the Date & Time helper connector (notice how the 'Format' has been deliberately set up to suit Salesforce protocols):

This image demonstrates the way one may use the SOSL query operation to run through 'All Fields' available, in order to return any and all account names and IDs found (unlike the above query example, this is tailored towards multiple )

For further information regarding the Salesforce Object Query Language (SOQL) and Salesforce Object Search Language please see their API Documentation site for more details.

Using the Raw HTTP Request ('Universal Operation')

IMPORTANT!: Please note that the Raw HTTP Request only works with REST API endpoints. You cannot make requests to SOAP API endpoints.

As of version 7.5, you can effectively create your own operations.

This is a very powerful feature which you can put to use when there is an endpoint in Salesforce which is not used by any of our operations.

To use this you will first of all need to research the endpoint in the Salesforce API documentation, to find the exact format that Salesforce will be expecting the endpoint to be passed in.

Note that you will only need to add the suffix to the endpoint, as the base URL will be automatically set (the base URL is picked up from the value you entered when you created your authentication).

IMPORTANT!: Accessing the base URL: If you need to access the base URL (e.g. for making a Full URL Raw HTTP requests) then the following information will be useful. The base URL for Salesforce is your Salesforce instance URL. It will most likely be similar to: 'https://abc123.salesforce.com' where 'abc123' is your Salesforce instance name. You may access your Salesforce instance URL within a workflow by using jsonpath to extract it from the authentication parameters '$.auth.instance_url'.

For example, say that the 'Get Job Info' operation did not exist in our Salesforce connector, and you wanted to use this endpoint, you would use the Salesforce API docs to find the relevant endpoint - which in this case is a GET request called: /services/data/vXX.X/jobs/ingest/jobID.

PLEASE NOTE: You will need to enter the correct API version (e.g. 'v51.0') and the correct job ID (e.g. '7504S000001nOJkQAM') in order to build a valid endpoint for the URL.

More details can be found here.

So if you know what your method, endpoint and details of your query parameters are, you can get the Salesforce job information with the following settings:

Method: GET

Endpoint: /services/data/v51.0/jobs/ingest/7504S000001nOJkQAM

Query Parameters: None

Body Type : None : null

Final Example outcome being: https://abc123.salesforce.com/services/data/v51.0/jobs/ingest/7504S000001nOJkQAM

All Operations

Latest version:

8.2