---
schema_version: "1"
id: 771714dfe0c4e6a65ad8a6e987b9d9cad91c3a7a1a151669abae8ce717addfdf
title: Step 3 - Create Apex Classes
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/data-ae/automate-resume-processing/guides/aes-create-apex-classes.md
canonical_url: https://developer.salesforce.com/docs/ai/automate-resume-processing/guide/aes-create-apex-classes.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 1b30b8944bcd5aea15ecfa2a861e95a6a33a4c621672181e39db9c2d131b2f36
products:
  - Automate Resume Processing
  - data-ae
---
# Step 3 - Create Apex Classes

Create two custom Apex classes to enable the Document AI flow functionality.

## 1. Create an Apex Class: Data 360 Document AI Flow Action

1. Go to the **Setup Menu**.
2. From the dropdown menu, select **Developer Console**.
3. In the Developer Console, click **File** > **New** > **Apex Class**.
4. Enter the class name: `de_DataCloudIDPFlowAction` and click **OK**.
5. 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.

```java
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('&quot;', '"').replaceAll('&amp;', '&');
            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

1. In the Developer Console, click **File** > **New** > **Apex Class**.
2. Enter the class name: `de_Extract_Document_AI_Details` and click **OK**.
3. When the code editor opens, paste this code and save the file.

```java
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_DataCloudIDPFlowAction`
- `dE_ExtractDocumentAIDetails`

## Next Step

- [Step 4 - Create the Screen Flow](https://developer.salesforce.com/docs/ai/automate-resume-processing/guide/aes-create-screen-flow.md): Create a new screen flow in Flow Builder to orchestrate Document AI and update the Contact record.
