Create two custom Apex classes to enable the Document AI flow functionality.
1. Create an Apex Class: Data 360 Document AI Flow Action
- Go to the Setup Menu.
- From the dropdown menu, select Developer Console.
- In the Developer Console, click File > New > Apex Class.
- Enter the class name:
de_DataCloudIDPFlowActionand click OK. - The code editor opens. Paste this code into the editor and save the file.
note: This is an out-of-the-box class that includes a method invoked by the flow you create on the next step. If you choose to use a different Apex class, make sure it includes such an invocable method.
public class de_DataCloudIDPFlowAction {
@InvocableMethod(label='Run DataCloud IDP Image/File Recognition with Schema Configuration')
public static List<Result> runImageProcessing(List<Request> requests) {
List<Result> results = new List<Result>();
for (Request req : requests) {
Result result = new Result();
try {
// 1. Get access token
String token = getAccessToken(req.Org_Domain, req.Client_Id, req.Client_Secret);
System.debug('Access Token >> ' + token);
// Vars
String orgDomain = req.Org_Domain;
String llmModel = req.LLM_Model;
String idpConfigId = req.IDPConfigId;
// 2. Get latest version ID as fileId
ContentVersion cv = [
SELECT Id
FROM ContentVersion
WHERE ContentDocumentId = :req.ContentDocumentId
ORDER BY VersionNumber DESC
LIMIT 1
];
String fileId = cv.Id;
System.debug('Using fileId >> ' + fileId);
// 3. Call schema generation or extract directly
String schemaJson;
String finalResponse;
if (String.isBlank(idpConfigId)) {
schemaJson = getSchemaJson(token, fileId, orgDomain, llmModel);
System.debug('Decoded Schema JSON >> ' + schemaJson);
finalResponse = extractData(token, fileId, schemaJson, orgDomain, llmModel, null);
} else {
finalResponse = extractData(token, fileId, null, orgDomain, llmModel, idpConfigId);
}
result.Status = 'Success';
result.ExtractedDataJson = finalResponse;
} catch (Exception e) {
result.Status = 'Failed';
result.ErrorMessage = e.getMessage();
System.debug('Error >> ' + e.getMessage());
}
results.add(result);
}
return results;
}
public class Request {
@InvocableVariable(label='Org Domain' required=true) public String Org_Domain;
@InvocableVariable(label='Client Id' required=true) public String Client_Id;
@InvocableVariable(label='Client Secret' required=true) public String Client_Secret;
@InvocableVariable(label='Document Type' required=true) public String Document_Type;
@InvocableVariable(label='LLM Model') public String LLM_Model;
@InvocableVariable(label='Content Document Id' required=true) public Id ContentDocumentId;
@InvocableVariable(label='IDPConfigId') public String IDPConfigId;
}
public class Result {
@InvocableVariable(label='Status') public String Status;
@InvocableVariable(label='Extracted Data (JSON)') public String ExtractedDataJson;
@InvocableVariable(label='Error Message (if any)') public String ErrorMessage;
}
// === 1. Get access token ===
private static String getAccessToken(String orgDomain, String clientId, String clientSecret) {
HttpRequest req = new HttpRequest();
req.setEndpoint(orgDomain + '/services/oauth2/token');
req.setMethod('POST');
req.setHeader('Content-Type','application/x-www-form-urlencoded');
req.setBody('grant_type=client_credentials&client_id=' + clientId + '&client_secret=' + clientSecret);
HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
Map<String,Object> resp = (Map<String,Object>)JSON.deserializeUntyped(res.getBody());
return (String)resp.get('access_token');
}
throw new CalloutException('Access Token Error: '+res.getBody());
}
// === 2. Generate schema ===
private static String getSchemaJson(String accessToken, String fileId, String orgDomain, String llmModel) {
HttpRequest req = new HttpRequest();
req.setEndpoint(orgDomain + '/services/data/v63.0/ssot/document-processing/actions/generate-schema');
req.setMethod('POST');
req.setHeader('Authorization','Bearer '+accessToken);
req.setHeader('Content-Type','application/json');
req.setHeader('Accept','application/json');
req.setHeader('Sforce-Call-Options','client=SchemaTest');
req.setTimeout(120000);
// Force hardcoded model for testing
llmModel = 'llmgateway__OpenAIGPT4Omni_08_06';
Map<String,Object> payload = new Map<String,Object>{
'mlModel' => llmModel,
'files' => new List<Object>{ new Map<String,Object>{ 'fileId' => fileId } }
};
String body = JSON.serialize(payload);
System.debug('Sending Schema Payload: ' + body);
req.setBody(body);
HttpResponse res = new Http().send(req);
System.debug('Schema Status Code: ' + res.getStatusCode());
System.debug('Schema Response: ' + res.getBody());
if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
Map<String,Object> bodyMap = (Map<String,Object>) JSON.deserializeUntyped(res.getBody());
String schemaRaw = (String) bodyMap.get('schema');
if (schemaRaw == null) {
throw new CalloutException('Schema Error: No "schema" field in response');
}
// Decode HTML escapes
String schemaDecoded = schemaRaw.replaceAll('"', '"').replaceAll('&', '&');
return schemaDecoded;
} else {
throw new CalloutException('Schema Error(' + res.getStatusCode() + '): ' + res.getBody());
}
}
// === 3. Extract data ===
private static String extractData(String accessToken, String fileId, String schemaJson,
String orgDomain, String llmModel, String idpConfigId) {
HttpRequest req = new HttpRequest();
req.setEndpoint(orgDomain + '/services/data/v63.0/ssot/document-processing/actions/extract-data');
req.setMethod('POST');
req.setHeader('Authorization','Bearer '+accessToken);
req.setHeader('Content-Type','application/json');
req.setHeader('Accept','application/json');
req.setTimeout(120000);
Map<String,Object> payload;
if (!String.isBlank(schemaJson)) {
payload = new Map<String,Object>{
'mlModel'=>llmModel,
'files'=>new List<Object>{ new Map<String,Object>{ 'fileId'=>fileId } },
'schemaConfig'=>schemaJson
};
} else {
payload = new Map<String,Object>{
'idpConfigurationIdOrName'=>idpConfigId,
'files'=>new List<Object>{ new Map<String,Object>{ 'fileId'=>fileId } }
};
}
System.debug('Extract Payload >> '+JSON.serializePretty(payload));
req.setBody(JSON.serialize(payload));
HttpResponse res = new Http().send(req);
System.debug('Extract Status >> '+res.getStatusCode());
System.debug('Extract Response >> '+res.getBody());
if (res.getStatusCode()>=200 && res.getStatusCode()<300) {
Map<String,Object> bodyMap = (Map<String,Object>)JSON.deserializeUntyped(res.getBody());
if (bodyMap.containsKey('data')) {
return JSON.serialize(bodyMap.get('data'));
}
return 'No data field in response';
}
throw new CalloutException('Extract Error('+res.getStatusCode()+'): '+res.getBody());
}
}
2. Create an Apex Class: Extract Resume Details with Document AI
- In the Developer Console, click File > New > Apex Class.
- Enter the class name:
de_Extract_Document_AI_Detailsand click OK. - When the code editor opens, paste this code and save the file.
public with sharing class de_Extract_Document_AI_Details {
// Wrapper for Flow Output
public class ExtractedDetails {
@InvocableVariable(label='First Name')
public String First_name;
@InvocableVariable(label='Last Name')
public String Last_name;
@InvocableVariable(label='Email Address of Candidate')
public String Email_address_of_candidate;
@InvocableVariable(label='Phone Number of Candidate')
public String Phone_number_of_candidate;
@InvocableVariable(label='Street Address of Candidate')
public String Street_address_of_candidate;
@InvocableVariable(label='City of Candidate')
public String City_of_candidate;
@InvocableVariable(label='State or Province of Candidate')
public String State_or_province_of_Candidate;
@InvocableVariable(label='Country of Candidate')
public String Country_of_candidate;
@InvocableVariable(label='Pincode or Zipcode of Candidate')
public String Pincode_or_zipcode_of_candidate;
@InvocableVariable(label='Summary of Qualifications')
public String Summary_of_qualifications;
}
// Flow Input Wrapper
public class DocumentInput {
@InvocableVariable(label='Document AI JSON or Raw Data' required=true
description='Raw JSON string or Document AI response to process')
public String inputKeyValueString;
@InvocableVariable(label='Prompt Template API Name' required=true
description='API name or ID of the Prompt Template (e.g., Handle_JSON_From_Document_AI or 0hfKZ000000PPrNYAW)')
public String promptTemplateApiName;
}
@InvocableMethod(
label='Extract Document AI Details'
description='Parses a Document AI response or key-value pair string into candidate details'
)
public static List<ExtractedDetails> extractDetails(List<DocumentInput> requests) {
List<ExtractedDetails> results = new List<ExtractedDetails>();
for (DocumentInput req : requests) {
ExtractedDetails output = new ExtractedDetails();
try {
// Validate that prompt template API name is provided
if (String.isBlank(req.promptTemplateApiName)) {
throw new AuraHandledException('Prompt Template API Name is required. Please provide the API name or ID of your prompt template.');
}
// === 1. Call Einstein Prompt Template to normalize the input ===
ConnectApi.EinsteinPromptTemplateGenerationsInput promptGenerationsInput =
new ConnectApi.EinsteinPromptTemplateGenerationsInput();
promptGenerationsInput.isPreview = false;
Map<String, ConnectApi.WrappedValue> valueMap = new Map<String, ConnectApi.WrappedValue>();
ConnectApi.WrappedValue wrappedInput = new ConnectApi.WrappedValue();
wrappedInput.value = req.inputKeyValueString;
valueMap.put('Input:inputString', wrappedInput);
promptGenerationsInput.inputParams = valueMap;
promptGenerationsInput.additionalConfig =
new ConnectApi.EinsteinLlmAdditionalConfigInput();
promptGenerationsInput.additionalConfig.applicationName = 'PromptTemplateGenerationsInvocable';
// Use the provided prompt template API name instead of hardcoded ID
ConnectApi.EinsteinPromptTemplateGenerationsRepresentation generationsOutput =
ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate(
req.promptTemplateApiName,
promptGenerationsInput
);
if (generationsOutput == null || generationsOutput.generations.isEmpty()) {
throw new AuraHandledException('No response returned from Einstein LLM. Please verify your Prompt Template is active and configured correctly.');
}
String rawResponse = generationsOutput.generations[0].text;
System.debug('Prompt response (raw): ' + rawResponse);
// === 2. Parse returned key-value pairs ===
List<String> pairs = rawResponse.split(';');
Map<String, String> kvMap = new Map<String, String>();
for (String pair : pairs) {
pair = pair.trim();
if (String.isNotBlank(pair) && pair.contains(':')) {
List<String> kv = pair.split(':', 2); // split only on first colon
String key = kv[0].trim();
String value = kv.size() > 1 ? kv[1].trim() : '';
kvMap.put(key, value);
}
}
// === 3. Map values to output fields ===
// Map based on exact keys from prompt template output
output.First_name = kvMap.get('First_name');
output.Last_name = kvMap.get('Last_name');
output.Email_address_of_candidate = kvMap.get('Email_address_of_candidate');
output.Phone_number_of_candidate = kvMap.get('Phone_number_of_candidate');
output.Street_address_of_candidate = kvMap.get('Street_address_of_candidate');
output.City_of_candidate = kvMap.get('City_of_candidate');
output.State_or_province_of_Candidate = kvMap.get('State_or_province_of_Candidate');
output.Country_of_candidate = kvMap.get('Country_of_candidate');
output.Pincode_or_zipcode_of_candidate = kvMap.get('Pincode_or_zipcode_of_candidate');
output.Summary_of_qualifications = kvMap.get('Summary_of_qualifications');
} catch (Exception e) {
System.debug('Error processing Document AI details: ' + e.getMessage());
System.debug('Stack trace: ' + e.getStackTraceString());
// Return empty results to allow flow to continue
}
results.add(output);
}
return results;
}
}
You now have two Apex classes to use in the next steps:
dE_DataCloudIDPFlowActiondE_ExtractDocumentAIDetails
Next Step
- Step 4 - Create the Screen Flow: Create a new screen flow in Flow Builder to orchestrate Document AI and update the Contact record.