How to Call Apex from a DataMapper in OmniStudio: FUNCTION() and Custom Functions Guide
The OmniStudio DataMapper (formerly DataRaptor) has built-in formula functions like IF, CONCAT, SUBSTRING, and ROUND that handle most data transformation needs. But in real-world projects, you will hit scenarios where native formulas are not enough: complex business rules, multi-step calculations, list processing, or logic you need to reuse across multiple DataMappers.
For these cases, OmniStudio provides two mechanisms to call Apex code directly from DataMapper formulas. This guide covers both approaches, with complete code examples, the dispatcher pattern, common errors, and when to use each one.
Know your org type first
Before implementing anything, you need to know whether your org uses Standard Runtime (OmniStudio runs natively on the platform, no managed package) or Managed Package Runtime (OmniStudio installed via a managed package like vlocity_cmt, vlocity_ins, or omnistudio). This determines which approach you can use.
| Feature | Standard Runtime | Managed Package |
|---|---|---|
| Apex interface | Callable (native Salesforce) |
Callable + VlocityOpenInterface |
| Function Definition (CMT) | Not available | Pre-installed with package |
| FUNCTION() formula | Yes (with Callable) | Yes (with Callable or VOI) |
| Call by function name only | Not available | Yes (via Function Definition) |
Approach 1: FUNCTION() with the Callable interface
This approach works on both Standard Runtime and Managed Package orgs. You call the Apex class explicitly in the formula.
Formula syntax
FUNCTION('ClassName.methodName', parameter)
FUNCTION('ClassName.methodName', param1, param2)
Apex class
The class must be global and implement the Callable interface:
global class WelcomeHandler implements Callable {
global Object call(String action, Map<String, Object> args) {
Map<String, Object> output = new Map<String, Object>();
try {
if (action == 'greet') {
String name = '';
if (args != null && args.containsKey('arguments')) {
Object argObj = args.get('arguments');
if (argObj instanceof List<Object>) {
List<Object> argList = (List<Object>) argObj;
if (!argList.isEmpty()) {
name = String.valueOf(argList[0]);
}
}
}
output.put('result',
String.isBlank(name)
? 'Welcome!'
: 'Welcome, ' + name.trim() + '!'
);
return output;
}
output.put('result', '');
return output;
} catch (Exception e) {
output.put('error', e.getMessage());
return output;
}
}
}
DataMapper formula
FUNCTION('WelcomeHandler.greet', Account:Name)
Note the colon separator (:) in the field path. In OmniStudio, Data JSON paths always use :, never dot notation.
output.put('result', value). If you skip this, the DataMapper receives an empty value with no error message. This is the #1 cause of "my function returns blank."
How parameters arrive
Parameters from the FUNCTION() formula arrive inside args under a key called "arguments" as a List<Object>. Always validate for null and cast carefully. The action parameter receives the method name from the formula (e.g., "greet").
Approach 2: Function Definition + VlocityOpenInterface
This approach is only available on Managed Package orgs. It allows you to write clean formulas without referencing the Apex class:
myCustomFunction(inputField)
No FUNCTION() wrapper. No class name. The magic happens through a Custom Metadata record that maps the function name to the Apex class.
Step 1: Create the Apex class
The class implements VlocityOpenInterface instead of Callable:
global class CustomFunctionHandler
implements vlocity_cmt.VlocityOpenInterface {
global Boolean invokeMethod(
String methodName,
Map<String, Object> input,
Map<String, Object> output,
Map<String, Object> options
) {
try {
if (methodName == 'formatCurrency') {
List<Object> args =
(List<Object>) input.get('arguments');
if (args == null || args.isEmpty()) {
output.put('result', 'R$ 0,00');
return true;
}
Decimal value = Decimal.valueOf(
String.valueOf(args[0])
);
output.put('result',
'R$ ' + value.setScale(2).format()
);
return true;
}
output.put('result', '');
return true;
} catch (Exception e) {
output.put('error', e.getMessage());
return false;
}
}
}
vlocity_cmt with your org's actual namespace (vlocity_ins, omnistudio, etc.).
Step 2: Create the Function Definition record
Go to Setup → Custom Metadata Types → Function Definition → Manage Records → New:
| Field | Value |
|---|---|
| Label | Format Currency |
| Function Definition Name | formatCurrency |
| ClassName | CustomFunctionHandler |
| MethodName | formatCurrency |
Step 3: Use in the DataMapper formula
formatCurrency(QuoteLineItem:UnitPrice)
That's it. The OmniStudio runtime reads the function name, looks it up in Function Definition metadata, finds the class, and calls invokeMethod.
The dispatcher pattern
A common misconception: people think each function needs its own Apex class. It does not. The invokeMethod approach is a dispatcher: one class can serve multiple functions. Just create one Function Definition record per function, all pointing to the same class with different MethodName values:
global Boolean invokeMethod(
String methodName,
Map<String, Object> input,
Map<String, Object> output,
Map<String, Object> options
) {
if (methodName == 'formatCurrency') {
// currency formatting logic
} else if (methodName == 'validateCPF') {
// CPF validation logic
} else if (methodName == 'calculateDiscount') {
// discount calculation logic
}
return true;
}
This keeps your codebase clean and your deploy simple: one class, multiple functions.
Common errors and troubleshooting
"You don't have the permission to perform this action"
The class is not global, does not implement the correct interface, the Function Definition record is missing, or the user's Profile/Permission Set does not grant access to the Apex class.
Empty result (blank field or {})
Almost always caused by a missing output.put('result', ...). Other causes: an unhandled exception that stops execution before the output is written, or the methodName not matching any branch in the if/else dispatcher.
Works in DEV, fails in QA or Production
This is the most dangerous error. The Apex class was deployed but the Function Definition Custom Metadata record was not. Or the namespace differs between environments. Always deploy both the class and the metadata record together, and validate in each target environment.
Function not found
In Approach 2: the name in the formula does not exactly match the Function Definition Name (it can be case-sensitive). In Approach 1: the class name is misspelled in the FUNCTION() formula.
When to use each approach
| Scenario | Recommended |
|---|---|
| Standard Runtime org (no managed package) | Approach 1: FUNCTION() + Callable |
| Managed Package org, needs clean formulas | Approach 2: Function Definition + VOI |
| Function reused across many DataMappers | Approach 2 (short name = easier reuse) |
| Quick, one-off function | Approach 1 (less setup) |
| Team cannot create Custom Metadata | Approach 1 (no metadata needed) |
| Native formula can solve it | Neither. Use native formulas. |
Performance considerations
Every custom function call executes Apex code, which consumes CPU time and counts toward Salesforce governor limits. If the DataMapper applies the function to each item in a list with hundreds of records, CPU time can spike quickly. Use custom functions only when native formulas genuinely cannot solve the problem.
Deploy checklist
Approach 1: deploy the Apex class + test class. The formula references the class directly, so no metadata is needed.
Approach 2: deploy the Apex class + test class + the Function Definition Custom Metadata record. If either is missing in the target environment, the function silently fails.
output.put('result', value). Always validate input parameters before casting. Always include both the Apex class and the Custom Metadata record in your deployment package.
Want to master OmniStudio DataMappers and every other component?
OmniStudio na Prática covers DataMappers, Integration Procedures, FlexCards, and OmniScripts in 130+ lessons. The most complete OmniStudio course available.
Work with meAlso check out OmniStudio for Energy & Utilities: Architecture Patterns and the OmniStudio 365 daily content project.