Sales Engagement Setup Guide
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();
}
This block is required in Sales Engagement.
The second logical block:
if (Trigger.isAfter && Trigger.isInsert) {
EmailMessageTriggerHelper.afterInsert(Trigger.new);
}
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
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
- Go to Setup > Change Data Capture.
- In the list of available objects, locate ActionCadenceStepTracker.
- Move it to Selected Entities.
- Save your changes!
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
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
- typically:
- 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
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.
Portal Documentation Links
Register the Resident Function in Salesforce
After the function is created in the portal, configure the corresponding Custom Metadata value in Salesforce:
- Go to Setup > Custom Metadata Types
- Open InCountryValue
- Click Manage InCountry Values
- Locate the record: CADENCE_TASK_RESIDENT_FUNCTION
- Click Edit
- In the field Value, enter the exact function name you created in the portal
- 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
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
Recordmust 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_defaultvalue 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
-
In Lightning App Builder, drag the component to the layout.

-
(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.
-
Activate the page layout.
