Skip to main content

Sales Engagement Setup Guide

info

The InCountry Sales Engagement component requires Sales Engagement to be configured in Salesforce. Sales Engagement must be set up according to the official Salesforce documentation.

⚠️ Note: The following steps from the Salesforce documentation are not required for this component:

  • Turn on Automated Actions

  • Set Up an Optional Sales Prospecting Bot

  • Start Using the Sales Engagement App

Prerequisites

  • Sales Engagement is enabled and configured in Salesforce (See the Info Panel above).
  • Configured Outbound Emails (Outbound (Chatter)).

Configuring Email Message Trigger

Trigger Logic Notes

The EmailMessageTrigger contains two logical blocks that serve different purposes and are not always both required.

The first logical block:

if (Trigger.isBefore && Trigger.isInsert) {
EmailMessageTriggerService.newInstance()
.setRecords(Trigger.new)
.processRecords();
}
note

This block is required in Sales Engagement.

The second logical block:

if (Trigger.isAfter && Trigger.isInsert) {
EmailMessageTriggerHelper.afterInsert(Trigger.new);
}
note

This block is required only if inbound emails (Email-to-Case) functionality is used (Inbound (Email-to-Case)).

  • If you do not use inbound emails (Email-to-Case), this logic is optional and can be omitted.

  • If inbound emails (email-to-case) is enabled, this block is mandatory.

Deploy Trigger

You must deploy the following Apex Trigger:

  • Apex Trigger: EmailMessageTrigger

Example package.xml

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>EmailMessageTrigger</members>
<name>ApexTrigger</name>
</types>
<version>65.0</version>
</Package>

Salesforce CLI Deployment Example:

Deploy metadata and run only the required test:

sf project deploy start \    
--manifest manifest/package.xml \
--test-level RunSpecifiedTests \
--tests EmailMessageTriggerHelperTest
note

For more details, please refer to the Salesforce CLI Documentation

Metadata

EmailMessageTrigger
trigger EmailMessageTrigger on EmailMessage (after insert) {

if (Trigger.isBefore && Trigger.isInsert) {
EmailMessageTriggerService.newInstance()
.setRecords(trigger.new)
.processRecords();
}

if (Trigger.isAfter && Trigger.isInsert) {
EmailMessageTriggerHelper.afterInsert(Trigger.new);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexTrigger>

Configuring Sales Engagement Activities Sync

Enable Change Data Capture for ActionCadenceStepTracker

  1. Go to Setup > Change Data Capture.
  2. In the list of available objects, locate ActionCadenceStepTracker.
  3. Move it to Selected Entities.
  4. Save your changes!

note

For more details, please refer to the Salesforce Change Data Capture Documentation

Deploy Apex Trigger, Handler, and Test Classes

You must deploy the following metadata components:

  • Apex Trigger: ActionCadenceStepTrackerTrigger
  • Apex Class: ActionCadenceStepTrackerTriggerHandler
  • Apex Test Class: ActionCadenceStepTrackerTriggerTestExample

Example package.xml

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>ActionCadenceStepTrackerTrigger</members>
<name>ApexTrigger</name>
</types>
<types>
<members>ActionCadenceStepTrackerTriggerHandler</members>
<members>ActionCadenceStepTrackerTriggerTest</members>
<name>ApexClass</name>
</types>
<version>65.0</version>
</Package>

Salesforce CLI Deployment Example:

Deploy metadata and run only the required test:

sf project deploy start \    
--manifest manifest/package.xml \
--test-level RunSpecifiedTests \
--tests ActionCadenceStepTrackerTriggerTest
note

For more details, please refer to the Salesforce CLI Documentation

Deploy PlatformEventSubscriberConfig

The PlatformEventSubscriberConfig must be deployed so that the Task-sync trigger runs under a specific user account.

This ensures that the trigger responsible for synchronizing Tasks has all required permissions, object access, and field-level rights when processing Change Data Capture events.

Before deployment, update the following fields:

  • platformEventConsumer — the name of the Apex Trigger that processes the CDC events
    • typically: ActionCadenceStepTrackerTrigger
    • if the trigger name was changed during deployment, use the updated name
  • user — the username of the user under whose permissions the sync logic will execute
    • should be user with full access required for InCountry Task synchronization

Example PlatformEventSubscriberConfig

<?xml version="1.0" encoding="UTF-8"?>
<PlatformEventSubscriberConfig xmlns="http://soap.sforce.com/2006/04/metadata">
<batchSize>200</batchSize>
<masterLabel>CadenceTriggerConfig</masterLabel>
<platformEventConsumer>ActionCadenceStepTrackerTrigger</platformEventConsumer>
<user>test-barvbhhycync@example.com</user>
</PlatformEventSubscriberConfig>

Example package.xml

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>CadenceTriggerNameConfig</members>
<name>PlatformEventSubscriberConfig</name>
</types>
<version>65.0</version>
</Package>

Salesforce CLI Deployment Example:

Deploy metadata and run only the required test:

sf project deploy start --manifest manifest/package.xml
note

For more details, please refer to the Salesforce Platform Event Trigger Documentation

Configure Resident Function in the Portal

To allow the system to execute the Task synchronization logic through the Resident Function, you must first create the function in the portal and then register it in Salesforce via Custom Metadata.

Register the Resident Function in Salesforce

After the function is created in the portal, configure the corresponding Custom Metadata value in Salesforce:

  1. Go to Setup > Custom Metadata Types
  2. Open InCountryValue
  3. Click Manage InCountry Values
  4. Locate the record: CADENCE_TASK_RESIDENT_FUNCTION
  5. Click Edit
  6. In the field Value, enter the exact function name you created in the portal
  7. Save the changes

This links the portal Resident Function with Salesforce, enabling the cadence processing logic to use it during Task synchronization.


Metadata

ActionCadenceStepTrackerTrigger
trigger ActionCadenceStepTrackerTrigger on ActionCadenceStepTrackerChangeEvent (after insert) {
ActionCadenceStepTrackerTriggerHandler.run(Trigger.new);
}
<?xml version="1.0" encoding="UTF-8"?>
<ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<status>Active</status>
</ApexTrigger>

ActionCadenceStepTrackerTriggerHandler
public with sharing class ActionCadenceStepTrackerTriggerHandler {
public static void run(List<ActionCadenceStepTrackerChangeEvent> events) {
Set<Id> actionCadenceStepTrackerIds = new Set<Id>();

for (ActionCadenceStepTrackerChangeEvent event : events) {

EventBus.ChangeEventHeader header = event.ChangeEventHeader;

for (Id recordId : header.getRecordIds()){
actionCadenceStepTrackerIds.add(recordId);
}
}

testIncountry1.CadenceTaskController.getInstance().executeSyncWithResidentFunction(actionCadenceStepTrackerIds);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<status>Active</status>
</ApexClass>

ActionCadenceStepTrackerTriggerTest
@IsTest
private class ActionCadenceStepTrackerTriggerTest {

private class FakeCadenceTaskController implements CadenceTaskController.ICadenceTaskController {
public Integer callCount = 0;

public void executeSyncWithResidentFunction(Set<Id> ids) {
this.callCount++;
}
}

@IsTest
static void testTriggerWithDummyEvents() {
Test.enableChangeDataCapture();

FakeCadenceTaskController fakeController = new FakeCadenceTaskController();
testIncountry1.CadenceTaskController.testInstance = fakeController;

EventBus.ChangeEventHeader createHeader = new EventBus.ChangeEventHeader();
createHeader.recordIds = new List<String>{ '8HFEk0000008JNFOA2' };
createHeader.changeType ='CREATE';
createHeader.entityName ='ActionCadenceStepTracker';
createHeader.changeOrigin ='user1-wsl';
createHeader.transactionKey = 'key';
createHeader.commitUser = UserInfo.getUserId();

ActionCadenceStepTrackerChangeEvent createEvent = new ActionCadenceStepTrackerChangeEvent();
createEvent.changeEventHeader = createHeader;
createEvent.put('ActionCadenceId', '77CEk000000A5BJMA0');
createEvent.put('ActionCadenceStepId', '8C8Ek0000001n6kKAA');
createEvent.put('State', 'Active');
createEvent.put('StepType', 'SendAnEmail');
createEvent.put('StepTitle', 'Email');

Test.startTest();
EventBus.publish(createEvent);
Test.getEventBus().deliver();
Test.stopTest();

Assert.areEqual(1, fakeController.callCount, 'executeSyncWithResidentFunction should be called');
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<status>Active</status>
</ApexClass>

CadenceTriggerConfig.platformEventSubscriberConfig
<?xml version="1.0" encoding="UTF-8"?>
<PlatformEventSubscriberConfig xmlns="http://soap.sforce.com/2006/04/metadata">
<batchSize>200</batchSize>
<masterLabel>CadenceTriggerConfig</masterLabel>
<platformEventConsumer>ActionCadenceStepTrackerTrigger</platformEventConsumer>
<user>user_name</user>
</PlatformEventSubscriberConfig>
Resident Function
module.exports.handler = async function(storage, country, params, modules) {

function safeParse(json, fallback) {
try {
if (!json) {
return fallback;
}
const parsed = JSON.parse(json);
return parsed ?? fallback;
} catch {
return fallback;
}
}

function parseParams(params) {
const data = safeParse(params.data, {});

const {
targetIds = [],
sfRecords = [],
regulatedFields = [],
keyFields = {}
} = data;

return {
targetIds: Array.isArray(targetIds) ? targetIds : [],
sfRecords: Array.isArray(sfRecords) ? sfRecords : [],
regulatedFields: Array.isArray(regulatedFields) ? regulatedFields : [],
keyFields: typeof keyFields === 'object' && keyFields !== null ? keyFields : {}
};
}

async function loadStorageRecords(storage, countryCode, targetIds) {
const filter = {
profile_key: targetIds
};
const options = {
limit: 100,
offset: 0
};
const result = await storage.find(countryCode, filter, options);

const map = new Map();

for (const rec of result.records || []) {
const body = safeParse(rec.body, {});
map.set(rec.profile_key, {
FirstName: body.FirstName || null,
LastName: body.LastName || null,
Company: body.Company || null
});
}

return map;
}

function parseSubjectString(subject) {
const regex = /^([^,]+),\s*([^,]+),\s*([^,]+)(?:,\s*([^,]+))?/;
const match = subject.match(regex);

if (!match) {
return null;
}

const [, typeAndSubType, cadenceName, targetName, companyName] = match;

return {
typeAndSubType,
cadenceName,
targetName,
companyName
};
}

function formatSubjectParts(parts) {
return parts.filter(Boolean).join(', ');
}

function buildSubject(task, related) {
const original = task.Subject;
if (!original) {
return original;
}

const parsed = parseSubjectString(original);
if (!parsed) {
return original;
}

const { typeAndSubType, cadenceName, targetName, companyName } = parsed;

const targetId = task.WhoId || task.WhatId;
const isLead = targetId?.startsWith('00Q');

const fullName =
[related?.FirstName, related?.LastName].filter(Boolean).join(' ') ||
targetName;

const company =
isLead ? (related?.Company || companyName || '') : undefined;

const parts = [typeAndSubType, cadenceName, fullName];

if (isLead) {
parts.push(company);
}

return formatSubjectParts(parts);
}

function buildRecord(task, related, regulatedFields, keyFields) {
const bodyData = {};
const targetId = task.WhoId || task.WhatId;

for (const field of regulatedFields) {
if (field === 'Id') continue;

if (field === 'Subject') {
bodyData[field] = buildSubject(task, related);
} else {
bodyData[field] = task[field];
}
}

const record = {
record_key: task.Id,
profile_key: task.Id,
service_key5: 'Task',
body: JSON.stringify(bodyData)
};

for (const [keyName, fieldName] of Object.entries(keyFields)) {
if (bodyData[fieldName] !== undefined) {
record[keyName] = bodyData[fieldName];
} else if (task[fieldName] !== undefined) {
record[keyName] = task[fieldName];
} else {
record[keyName] = null;
}
}

return record;
}

const {
targetIds,
sfRecords,
regulatedFields,
keyFields
} = parseParams(params);
const countryCode = country;

let recordMap
try {
recordMap = await loadStorageRecords(storage, countryCode, targetIds);
} catch (error) {
return {
success: false,
error: true,
errorMsg: `Failed to load storage records: ${error}`,
};
}

let updatedRecords;
try {
updatedRecords = sfRecords.map(task => {
const targetId = task.WhoId || task.WhatId;
const related = recordMap.get(targetId);
return buildRecord(task, related, regulatedFields, keyFields);
});
} catch (error) {
return {
success: false,
error: true,
errorMsg: `Failed to build updated records: ${error}`,
};
}

let writeResults;
try {
writeResults = await storage.batchWrite(countryCode, updatedRecords);
} catch (error) {
return {
success: false,
error: true,
errorMsg: `Batch write failed: ${error}`,
};
}

return {
success: true,
error: false,
data: JSON.stringify(writeResults)
};

};

Deploy Sales Engagement Flows

Deploy Flows

You must deploy the following flows:

  • AssignTargetToSalesCadence
  • ChangeSalesCadenceTargetAssignee
  • ModifyCadenceTrackerAttributesAction
  • PauseSalesCadenceTracker
  • RemoveTargetFromSalesCadence
  • SendSalesCadenceEvent

Example package.xml

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>AssignTargetToSalesCadence</members>
<members>ChangeSalesCadenceTargetAssignee</members>
<members>PauseSalesCadenceTracker</members>
<members>RemoveTargetFromSalesCadence</members>
<members>SendSalesCadenceEvent</members>
<name>Flow</name>
</types>
<version>65.0</version>
</Package>

Salesforce CLI Deployment Example:

Deploy metadata and run only the required test:

sf project deploy start --manifest manifest/package.xml
note

For more details, please refer to the Salesforce CLI Documentation


Metadata

Download assigntargettosalescadence.flow-meta.xml.

Download changesalescadencetargetassignee.flow-meta.xml.

Download modifycadencetrackerattributesaction.flow-meta.xml.

Download pausesalescadencetracker.flow-meta.xml.

Download removetargetfromsalescadence.flow-meta.xml.

Download sendsalescadenceevent.flow-meta.xml.

Configuring Email Templates for Automated Email Steps

The Sales Engagement component supports two types of merge fields:

  • Regulated merge fields (InCountry-synchronized)
  • Non-regulated merge fields (standard Salesforce merge fields)

Regulated Merge Fields

Regulated merge fields must be used for data synchronized with InCountry.

Format

%profile_key=<RecordId>,<FieldName>,inc_default:{{{FallbackValue}}}:inc_default%

Requirements

  • The Record must contain an 18-character Salesforce record ID.
  • The ID must be generated using a formula field:
CASESAFEID(Id)
  • <FieldName> must match the InCountry field name.
  • The inc_default value is used as a fallback if no record is found in InCountry.

Example

%profile_key={{{Recipient.Id_18__c}}},LastName,inc_default:{{{Recipient.LastName}}}:inc_default%

Non-Regulated Merge Fields

Non-regulated fields use standard Salesforce merge field syntax and are not synchronized with InCountry.

Format

{{{Recipient.FirstName}}}
{{{Sender.LastName}}}

Example Email Template

Dear %profile_key={{{Recipient.Id_18__c}}},LastName,inc_default:{{{Recipient.LastName}}}:inc_default% %profile_key={{{Recipient.Id_18__c}}},FirstName,inc_default:{{{Recipient.FirstName}}}:inc_default%

Thank you for contacting customer support {{{Sender.CompanyName}}}.
We are pleased to inform you that a case has been created based on your request.
Our experts will contact you soon.

Best Regards,

{{{Sender.Name}}}

{{{Sender.CompanyName}}}

{{{Sender.Address}}}

{{{Sender.Email}}}


Configuring the InCountry Sales Engagement component

  1. In Lightning App Builder, drag the component to the layout.

  2. (Optional) Select the component to open the settings in the right part of the screen. In the Set Component Visibility expand block, click + Add Filter. Define the filtration criteria for the component visibility on the layout according to your needs.

  3. Activate the page layout.