One of the most powerful aspects of OmniStudio in Salesforce is the ability to use Integration Procedures and Data Mappers from within Apex code. This gives developers the flexibility to leverage these declarative tools while maintaining the control and logic that only Apex can provide. Whether you’re building complex automations, batch jobs, or custom business logic, calling Integration Procedures and Data Mappers from Apex opens up countless possibilities.
In this guide, we’ll walk through exactly how to call both Integration Procedures and Data Mappers from your Apex classes, with real code examples, best practices, and troubleshooting tips. By the end, you’ll have a solid foundation for integrating these OmniStudio components into your Apex workflows.
Why Call Integration Procedures and Data Mappers from Apex?
Before diving into the how, let’s understand the why. There are several compelling reasons to invoke these OmniStudio components from Apex:
Reusability – You build an Integration Procedure once and call it from multiple places: OmniScripts, REST APIs, Flows, and Apex classes. This eliminates code duplication and makes maintenance easier.
Declarative Power – Integration Procedures and Data Mappers are built using a visual interface with drag-and-drop components. This means non-developers can contribute to business logic, and changes can be made without recompiling code.
Complex Data Transformations – Data Mappers excel at extracting and transforming Salesforce data into structured JSON. Instead of writing SOQL queries and data transformation logic in Apex, you can leverage Data Mapper’s built-in functions and formulas.
Separation of Concerns – Your Apex code focuses on orchestration and business logic, while Integration Procedures handle data retrieval and integration with external systems. This makes code cleaner and easier to test.
Error Handling – Integration Procedures include built-in error handling with Try-Catch blocks, which can be more readable and maintainable than nested Apex try-catch structures.
Understanding the Namespace: omnistudio vs vlocity_cmt
Before you start calling anything from Apex, you need to understand which namespace to use. This depends on which version of OmniStudio your Salesforce org has installed.
For OmniStudio (Standard Runtime) – Use the omnistudio namespace. This is the newer, cloud-native version of OmniStudio that uses Salesforce standard objects. If your org is on a recent Salesforce release with OmniStudio, this is what you’ll use.
For Salesforce Industries (Managed Package Runtime) – Use the vlocity_cmt namespace. This is the original managed package version that predates OmniStudio and is still widely used in industry implementations like Communications, Health, and Insurance.
To check which version your org has, go to Setup > Installed Packages and look for either “OmniStudio” or “Vlocity Industries” (or similar). The active package determines which namespace you’ll use in your Apex code.
In the examples below, we’ll show both approaches. If you’re using OmniStudio standard runtime, use omnistudio. If you’re on Salesforce Industries (Vlocity), use vlocity_cmt.
Part 1: Calling an Integration Procedure from Apex
Step 1: Understand Your Integration Procedure’s Type and SubType
Before you can call an Integration Procedure from Apex, you need to know two pieces of information:
- Type – The first part of the Integration Procedure identifier
- SubType – The second part of the Integration Procedure identifier
In OmniStudio, Integration Procedures are identified by the format Type_SubType. For example, if you have an Integration Procedure named “get_contactdata”, the Type might be “get” and the SubType might be “contactdata”. When called from Apex, you’ll reference it as “get_contactdata” using the naming convention.
To find your Integration Procedure’s Type and SubType, navigate to OmniStudio > Integration Procedures and open your procedure. You’ll see the Type and SubType fields displayed in the procedure configuration.
Step 2: Create Maps for Input, Output, and Options
The runIntegrationService method requires three maps:
Map<String, Object> ipInput = new Map<String, Object>();
Map<String, Object> ipOutput = new Map<String, Object>();
Map<String, Object> ipOptions = new Map<String, Object>();
ipInput Map – Contains the data your Integration Procedure expects as input. For example, if your Integration Procedure needs a Contact ID to fetch contact details, you’ll put that here.
ipOutput Map – This is where the result of the Integration Procedure execution will be stored. It’s passed by reference, so the method will populate it with the response.
ipOptions Map – Contains configuration options for how the Integration Procedure should behave. Common options include:
- isDebug – Set to true if you want detailed debug information
- chainable – Set to true if the Integration Procedure is configured for chaining (handling long-running procedures)
- queueableChainable – Set to true for asynchronous execution
Even if you don’t need any options, the options map must be passed (it can be empty).
Step 3: Populate the Input Map
Add the data your Integration Procedure needs to the input map. The structure depends entirely on what your procedure expects.
String recordId = '0011y00000abcde';
ipInput.put('recordId', recordId);
If your Integration Procedure expects nested data, you can create nested maps:
Map<String, Object> contactData = new Map<String, Object>();
contactData.put('firstName', 'John');
contactData.put('lastName', 'Doe');
ipInput.put('contact', contactData);
Step 4: Call the Integration Procedure
Now comes the actual call. The syntax is:
ipOutput = (Map<String, Object>) omnistudio.IntegrationProcedureService.runIntegrationService(
'Type_SubType',
ipInput,
ipOptions
);
Or for Salesforce Industries (Vlocity):
ipOutput = (Map<String, Object>) vlocity_cmt.IntegrationProcedureService.runIntegrationService(
'Type_SubType',
ipInput,
ipOptions
);
Breaking this down:
- Replace ‘Type_SubType’ with your actual Integration Procedure identifier (e.g., ‘get_contactdata’)
- The method returns an Object, so we cast it to Map<String, Object>
- Pass your input map and options map as parameters
Step 5: Handle the Response
After the call completes, your response will be in the ipOutput map. You can now work with it:
// Log the entire response
System.debug('IP Output: ' + JSON.serialize(ipOutput));
// Extract specific values
String contactName = (String) ipOutput.get('contactName');
Boolean isSuccess = (Boolean) ipOutput.get('success');
// Handle nested data
Map<String, Object> contactDetails = (Map<String, Object>) ipOutput.get('contactData');
Complete Example: Calling an Integration Procedure
Here’s a complete, realistic example:
public class ContactDataFetcher {
public static void fetchContactDetails(String recordId) {
try {
// Step 1: Create maps
Map<String, Object> ipInput = new Map<String, Object>();
Map<String, Object> ipOutput = new Map<String, Object>();
Map<String, Object> ipOptions = new Map<String, Object>();
// Step 2: Populate input
ipInput.put('recordId', recordId);
// Step 3: Call the Integration Procedure
ipOutput = (Map<String, Object>) omnistudio.IntegrationProcedureService.runIntegrationService(
'get_contactdata',
ipInput,
ipOptions
);
// Step 4: Handle response
System.debug('Response from Integration Procedure: ' + JSON.serialize(ipOutput));
// Extract data from response
String contactName = (String) ipOutput.get('name');
String email = (String) ipOutput.get('email');
// Use the extracted data
System.debug('Contact Name: ' + contactName);
System.debug('Email: ' + email);
} catch (Exception e) {
System.debug('Error calling Integration Procedure: ' + e.getMessage());
System.debug('Stack trace: ' + e.getStackTraceString());
}
}
}
Part 2: Calling a Data Mapper from Apex
Data Mappers work slightly differently from Integration Procedures. Instead of using runIntegrationService, you’ll use the DRGlobal class from the Vlocity or OmniStudio package.
Understanding Data Mapper Types
Before calling a Data Mapper, know that Data Mappers come in three flavors:
- Extract – Reads data from Salesforce objects and returns results in JSON format
- Load – Writes data back to Salesforce objects from JSON input
- Transform – Performs intermediate data transformations without hitting the database
From Apex, you can call all three types using the same method.
Step 1: Determine Your Data Mapper Name
Find your Data Mapper’s exact name by navigating to OmniStudio > Data Mapper and opening it. The name you use in your Apex code is exactly what appears in the Data Mapper list (e.g., ‘ExtractContactInfo’, ‘GetUserDetails’).
Step 2: Create Your Input Data
Data Mappers accept input as either a Map<String, Object> or as a JSON string. We’ll use maps for this example, as they’re type-safe:
Map<String, Object> dmInput = new Map<String, Object>();
dmInput.put('userId', 'userId12345');
dmInput.put('includeOrders', true);
Step 3: Call the Data Mapper
The method syntax is:
vlocity_ins.DRProcessResult result = vlocity_ins.DRGlobal.process(dmInput, 'DataMapperName');
Or using the newer standard runtime:
omnistudio.DRProcessResult result = omnistudio.DRGlobal.process(dmInput, 'DataMapperName');
The method returns a DRProcessResult object, which we then need to convert to a usable format.
Step 4: Process the Result
The tricky part with Data Mappers is that the result comes back as a DRProcessResult object, which needs to be deserialized. Here are the common ways to handle it:
For Extract Data Mappers (returning a list of records):
List<Map<String, Object>> extractResult = (List<Map<String, Object>>) result.toJsonList();
For Transform or Extract (returning a single map):
Map<String, Object> transformResult = result.convertToMap();
To serialize and debug:
System.debug('Data Mapper result: ' + JSON.serialize(result.convertToMap()));
Complete Example: Calling a Data Mapper
Here’s a full example of calling an Extract Data Mapper:
public class UserDataExtractor {
public static void extractUserInfo(String userId) {
try {
// Step 1: Create input map
Map<String, Object> dmInput = new Map<String, Object>();
dmInput.put('userId', userId);
// Step 2: Call the Data Mapper
vlocity_ins.DRProcessResult result = vlocity_ins.DRGlobal.process(dmInput, 'GetUserForContact');
// Step 3: Convert result to map
List<Map<String, Object>> userList = (List<Map<String, Object>>) result.toJsonList();
// Step 4: Process results
if (userList != null && userList.size() > 0) {
Map<String, Object> userMap = userList.get(0);
String userName = (String) userMap.get('Name');
String userEmail = (String) userMap.get('Email');
System.debug('User Name: ' + userName);
System.debug('User Email: ' + userEmail);
} else {
System.debug('No user data found');
}
} catch (Exception e) {
System.debug('Error calling Data Mapper: ' + e.getMessage());
}
}
}
JSON String Input Alternative
If you prefer passing input as a JSON string instead of a map, you can do this:
String dataJson = '{"userId":"userId12345","includeOrders":true}';
vlocity_ins.DRProcessResult result = vlocity_ins.DRGlobal.processObjectsJSON(dataJson, 'GetUserForContact');
This approach is useful when you already have JSON data from an API call or another source.
Part 3: Advanced Patterns – Integration Procedures Calling Data Mappers
A common pattern in OmniStudio development is having an Integration Procedure call a Data Mapper internally. But what if you need to do this from Apex? You would:
- Call the Integration Procedure from Apex (which internally calls a Data Mapper)
- The result returned to Apex will already include the Data Mapper’s processed data
This is cleaner than calling both separately, as the Integration Procedure handles orchestration. However, if you need more control, you can call them sequentially from Apex:
// First, call Data Mapper to extract data
Map<String, Object> dmInput = new Map<String, Object>();
dmInput.put('recordId', '0011y00000abcde');
vlocity_ins.DRProcessResult dmResult = vlocity_ins.DRGlobal.process(dmInput, 'ExtractContactData');
List<Map<String, Object>> contactData = (List<Map<String, Object>>) dmResult.toJsonList();
// Then, pass that data to an Integration Procedure
Map<String, Object> ipInput = new Map<String, Object>();
ipInput.put('contactData', contactData.get(0));
Map<String, Object> ipOutput = (Map<String, Object>) vlocity_cmt.IntegrationProcedureService.runIntegrationService(
'process_contact',
ipInput,
new Map<String, Object>()
);
System.debug('Final result: ' + JSON.serialize(ipOutput));
Handling Data: Serialization and Deserialization
When working with Integration Procedures and Data Mappers from Apex, you’ll frequently need to convert between different data formats. Here’s what you need to know:
JSON Serialization
When you have a Map or object and need to convert it to JSON string:
Map<String, Object> data = new Map<String, Object>();
data.put('name', 'John Doe');
data.put('email', 'john@example.com');
String jsonString = JSON.serialize(data);
System.debug(jsonString); // {"name":"John Doe","email":"john@example.com"}
JSON Deserialization
When you have a JSON string and need to convert it to a map:
String jsonString = '{"name":"John Doe","email":"john@example.com"}';
Map<String, Object> data = (Map<String, Object>) JSON.deserializeUntyped(jsonString);
String name = (String) data.get('name');
Working with Lists
Data Mappers often return lists of records. Handle them carefully:
List<Map<String, Object>> records = (List<Map<String, Object>>) result.toJsonList();
for (Map<String, Object> record : records) {
String id = (String) record.get('Id');
String name = (String) record.get('Name');
System.debug('Record: ' + id + ' - ' + name);
}
Error Handling and Debugging
Things can go wrong, and knowing how to troubleshoot is essential.
Common Errors and Solutions
Error: “Type not found: omnistudio.IntegrationProcedureService”
- Solution: Your org doesn’t have OmniStudio installed. Check Setup > Installed Packages. Use vlocity_cmt instead if you have Salesforce Industries.
Error: “Unknown named argument: runIntegrationService”
- Solution: You’re using the wrong namespace or syntax. Verify you’re using the correct package namespace for your org.
Error: “Null pointer exception when accessing output data”
- Solution: The Integration Procedure or Data Mapper didn’t return expected data. Add debug logging to see what actually returned:
System.debug('Full output: ' + JSON.serialize(ipOutput));
Error: “Governor limit exceeded”
- Solution: You’re hitting Salesforce’s governor limits. If calling from Batch Apex or in a loop, consider:
- Bulkifying your code (processing multiple records in one call instead of many calls)
- Using chainable Integration Procedures (check the Chainable option in your IP configuration)
- Setting chainable: true in your options map if the IP supports it
Debugging Techniques
Enable Debug Output:
Map<String, Object> ipOptions = new Map<String, Object>();
ipOptions.put('isDebug', true);
Map<String, Object> ipOutput = (Map<String, Object>) omnistudio.IntegrationProcedureService.runIntegrationService(
'get_contactdata',
ipInput,
ipOptions
);
This adds detailed debugging information to the response.
Log Everything:
System.debug('Input: ' + JSON.serialize(ipInput));
System.debug('Output: ' + JSON.serialize(ipOutput));
System.debug('Output keys: ' + ipOutput.keySet());
Use Execute Anonymous:
In the Developer Console, use Debug > Open Execute Anonymous Window to quickly test your Apex code without creating a class:
Map<String, Object> dmInput = new Map<String, Object>();
dmInput.put('userId', 'test123');
vlocity_ins.DRProcessResult result = vlocity_ins.DRGlobal.process(dmInput, 'GetUserData');
System.debug('Result: ' + JSON.serialize(result.convertToMap()));
Check Debug Logs:
Always review debug logs to understand the flow. Go to Setup > Debug Logs to enable logging for your user, then run your code and review the logs. Look for:
- Any exceptions thrown
- The actual values being passed and returned
- Timing information to spot performance issues
Best Practices
When calling Integration Procedures and Data Mappers from Apex, follow these practices:
1. Always Use Try-Catch Blocks
Integration Procedures and Data Mappers can throw exceptions. Always wrap your calls in try-catch:
try {
Map<String, Object> ipOutput = (Map<String, Object>) omnistudio.IntegrationProcedureService.runIntegrationService(
'get_data',
ipInput,
ipOptions
);
} catch (Exception e) {
System.debug('Error: ' + e.getMessage());
System.debug('Stack: ' + e.getStackTraceString());
// Handle the error appropriately
}
2. Validate Input Data
Don’t assume your input map will be populated correctly. Validate:
if (String.isBlank(recordId)) {
throw new IllegalArgumentException('Record ID is required');
}
if (ipInput == null || ipInput.isEmpty()) {
throw new IllegalArgumentException('Input data is required');
}
3. Check Response Data Types
Maps can contain various data types. Always cast carefully:
Object rawValue = ipOutput.get('someField');
if (rawValue instanceof String) {
String stringValue = (String) rawValue;
} else if (rawValue instanceof Integer) {
Integer intValue = (Integer) rawValue;
} else if (rawValue instanceof List<Object>) {
List<Object> listValue = (List<Object>) rawValue;
}
4. Limit Data Returned
If your Data Mapper can return thousands of records, paginate or filter:
// In your Data Mapper, add a Limit clause
// Or from Apex, check the size:
List<Map<String, Object>> results = (List<Map<String, Object>>) result.toJsonList();
if (results.size() > 1000) {
System.debug('Warning: Large result set detected');
// Handle accordingly
}
5. Use Meaningful Variable Names
Make your code readable:
// Good
Map<String, Object> contactDataInput = new Map<String, Object>();
Map<String, Object> contactDataOutput = new Map<String, Object>();
// Confusing
Map<String, Object> m1 = new Map<String, Object>();
Map<String, Object> m2 = new Map<String, Object>();
6. Document the Expected Input/Output
Add comments explaining what the Integration Procedure or Data Mapper expects and returns:
/**
* Fetches contact data from an Integration Procedure
* Expected Input: recordId (String) - The ID of the contact record
* Expected Output:
* - contactName (String)
* - contactEmail (String)
* - contactPhone (String)
*/
public static Map<String, Object> getContactData(String recordId) {
// Implementation
}
Performance Considerations
When calling Integration Procedures and Data Mappers from Apex at scale, consider:
Governor Limits
Each Integration Procedure or Data Mapper call consumes resources:
- SOQL queries (100 per sync transaction)
- DML operations (150 per sync transaction)
- CPU time (10 seconds per sync transaction)
- Heap size (6 MB per sync transaction)
Monitor your Limits:
System.debug('SOQL Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML Rows: ' + Limits.getDmlRows() + '/' + Limits.getLimitDmlRows());
System.debug(‘SOQL Queries: ‘ + Limits.getQueries() + ‘/’ + Limits.getLimitQueries());
System.debug(‘DML Rows: ‘ + Limits.getDmlRows() + ‘/’ + Limits.getLimitDmlRows());
Asynchronous Processing
For large-scale operations, consider using Batch Apex or Queueable Apex to process data asynchronously and reset governor limits between batches:
global class DataProcessorBatch implements Database.Batchable<SObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id FROM Contact WHERE CreatedDate = TODAY');
}
global void execute(Database.BatchableContext bc, List<Contact> contacts) {
for (Contact c : contacts) {
// Call Integration Procedure or Data Mapper here
// Governor limits reset for each batch
}
}
global void finish(Database.BatchableContext bc) {
// Handle completion
}
}
Caching Results
If calling the same Integration Procedure or Data Mapper repeatedly with the same inputs, cache the results:
private static Map<String, Map<String, Object>> resultCache = new Map<String, Map<String, Object>>();
public static Map<String, Object> getContactDataCached(String recordId) {
if (resultCache.containsKey(recordId)) {
return resultCache.get(recordId);
}
Map<String, Object> result = getContactData(recordId);
resultCache.put(recordId, result);
return result;
}
Testing Integration Procedures and Data Mappers
Writing test classes for Apex code that calls Integration Procedures can be challenging because these components may make external calls. Here’s a testing approach:
Unit Test with Mocking
@isTest
private class ContactDataFetcherTest {
@isTest
static void testFetchContactDetails() {
// Arrange
String recordId = '0011y00000abcde';
// Act
Test.startTest();
ContactDataFetcher.fetchContactDetails(recordId);
Test.stopTest();
// Assert
// Check that expected behavior occurred
// (Note: Testing Integration Procedures in unit tests is limited without mocking)
}
}
For more advanced testing, consider mocking external callouts or creating integration test environments.
Real-World Scenarios
Scenario 1: Processing Orders from an External System
You receive orders from an external system and need to create Order records in Salesforce with related Order Items.
public class OrderProcessor {
public static void processExternalOrder(String externalOrderJson) {
try {
// Parse the JSON
Map<String, Object> externalOrder = (Map<String, Object>) JSON.deserializeUntyped(externalOrderJson);
// Call Integration Procedure to validate and transform data
Map<String, Object> ipInput = new Map<String, Object>();
ipInput.put('externalOrder', externalOrder);
Map<String, Object> ipOutput = (Map<String, Object>) vlocity_cmt.IntegrationProcedureService.runIntegrationService(
'order_process',
ipInput,
new Map<String, Object>()
);
// Check if processing was successful
Boolean isSuccess = (Boolean) ipOutput.get('success');
if (isSuccess) {
String salesforceOrderId = (String) ipOutput.get('orderId');
System.debug('Order created successfully: ' + salesforceOrderId);
} else {
String errorMessage = (String) ipOutput.get('errorMessage');
System.debug('Order processing failed: ' + errorMessage);
}
} catch (Exception e) {
System.debug('Unexpected error: ' + e.getMessage());
}
}
}
Scenario 2: Extracting Data for Reporting
You need to extract complex related data for a custom report:
public class ReportDataExtractor {
public static List<Map<String, Object>> getAccountWithContacts(String accountId) {
try {
// Use Data Mapper to extract account and related contacts
Map<String, Object> dmInput = new Map<String, Object>();
dmInput.put('accountId', accountId);
vlocity_ins.DRProcessResult result = vlocity_ins.DRGlobal.process(dmInput, 'ExtractAccountWithContacts');
List<Map<String, Object>> accountData = (List<Map<String, Object>>) result.toJsonList();
System.debug('Extracted ' + accountData.size() + ' records');
return accountData;
} catch (Exception e) {
System.debug('Error extracting data: ' + e.getMessage());
return new List<Map<String, Object>>();
}
}
}
Conclusion
Calling Integration Procedures and Data Mappers from Apex gives you the best of both worlds: the declarative power and flexibility of OmniStudio components combined with the control and logic of Apex. By following the patterns and best practices outlined in this guide, you can build robust, maintainable, and scalable solutions.
Key takeaways:
- Use IntegrationProcedureService.runIntegrationService() for calling Integration Procedures
- Use DRGlobal.process() for calling Data Mappers
- Always validate input, handle exceptions, and debug thoroughly
- Consider performance implications, especially at scale
- Test your code in sandboxes before deploying to production
With this knowledge, you’re ready to integrate OmniStudio components into your Apex workflows and unlock new possibilities in your Salesforce implementations.
Watch the Complete Tutorial
Want to see this in action? Check out our step-by-step video tutorial that walks through calling Integration Procedures and Data Mappers from Apex with live coding demonstrations and real-world examples.
Watch on YouTube:
In this video, you’ll see:
- How to identify Integration Procedure Type and SubType
- Complete code examples for calling both Integration Procedures and Data Mappers
- Real-time response handling and debugging
- Common mistakes and how to avoid them
- Performance optimization techniques
Ready to Master OmniStudio Apex Integration?
If this guide helped you understand how to leverage Integration Procedures and Data Mappers in your Apex code, you’re on the right track. But OmniStudio mastery goes beyond just calling components – it requires understanding the entire ecosystem.
What’s Next?
Explore more Salesforce tutorials on our blog:
- Batch Apex in Salesforce: Best Practices & Use Cases – Process millions of records efficiently
- Governor Limits in Salesforce: Complete Guide – Understand constraints and optimize your code
- OmniStudio FlexCards Beginner’s Guide – Build stunning user interfaces
- Embedding FlexCards in Lightning Web Components – Advanced LWC integration patterns
Connect With 2Creative Solutions
Have questions about OmniStudio, Apex development, or Salesforce implementation? We’re here to help.
📞 Book a Free Consultation: https://2creative.ca/
Our Salesforce experts can help you:
- Design custom OmniStudio solutions
- Optimize Apex code for performance
- Implement complex Salesforce Industries projects
- Train your team on best practices
Subscribe to Our YouTube Channel:
https://www.youtube.com/@2CreativeSolutions
Get regular Salesforce tutorials, tips, and deep dives into OmniStudio, Apex development, and Salesforce best practices.
🤝 Connect on Social Media:
- LinkedIn: https://www.linkedin.com/company/2cretiv