Exploring File Processing Capabilities in Salesforce - Prompt Builder
In this blog post series, we will explore the different file processing capabilities available in Salesforce, including:
-
Prompt Builder
-
Document AI
-
Intelligent Document Reader
Before diving into the implementation details, let’s start with a comparison of these options to understand their unique features and use cases.
Comparison:
Use Case: Automating Case Status Update with Prompt Builder
Let’s implement the following use case using Prompt Builder:
A customer has purchased a laptop and wants to create a case for a hardware issue — for example, the battery is not working. The customer sends an email to the support address, and the system should automatically create a case in Salesforce.
Along with the email, the customer attaches two documents:
-
Product Invoice
-
Warranty Card
The requirement is to parse both documents and extract the following key details:
-
Customer Name
-
Product Description
-
Serial Number
-
Date of Purchase (from the invoice)
-
Warranty-related details (from the warranty card)
Once the data is extracted, it must be validated against Salesforce records:
-
Check for a valid Asset record matching the provided serial number.
-
If no matching asset is found, update the Case Product Status (custom field) as “Product Not Found.”
-
-
If a valid asset exists, verify whether it falls within the warranty period by checking the related Entitlement record.
-
If the asset is within the warranty period, update Case Product Status to “In Warranty.”
-
Otherwise, update it to “Out of Warranty.”
-
Implementation Details
- Set up Email to Case
- Create Prompt Builder Template for file parsing
- Record Triggered Flow on Case creation
- Invocable Apex Class Creation for json parsing and validations
- Set up Email to Case
If you have not done this in the past, please follow this Salesforce help article to understand how to set up Email to Case.
- Create Prompt Builder Template for file parsing
Create a new prompt template of type flex:
Keep the default on model selection and proceed with creating the prompt.
Make sure to use insert resource and select the related atatchments(combined attachments) to get the files for processing.
In the prompt ask the builder to retrieve the data in json format so that the flow can then process this.
Now let us preview this prompt builder action by giving a test case record.
For that either use emailtocase or manually create a case with specific attachments.
Laptop invoice:
Now, click on preview in the prmpt builder and enter the created case number:
This makes sure that the prompt is working as expected.
- Record Triggered Flow on Case creation
Now let us create a record triggered flow on case to call the prompt template for processing the attachments in the case.
Keep the criteria for flow execution simple so that it’s easy to test the setup.
-
Add an Action — Create an action named RetrieveWarrantyDetails to invoke the Prompt Builder template for processing the attached files.
-
Pass Parameters — Pass the Case record as a parameter to the action.
-
Store Response — Capture the response JSON text from the Prompt Builder in a flow variable (e.g., response).
- Invocable Apex Class Creation
To handle the JSON parsing and required validations, I have created an Invocable Apex Class (fully generated using GitHub Copilot).
This class accepts the JSON text and Case as input parameters, performs the necessary validations, and updates the product status accordingly.***************public with sharing class CaseWarrantyFlowHandler { // Inner class to represent the product details structure public class ProductDetails { public String CustomerName; public String ProductDescription; public String SerialNumber; public Date DateOfPurchase; public Date WarrantyStartDate; public Date WarrantyEndDate; } // Request wrapper class for Flow input public class Request { @InvocableVariable(required=true) public Id caseId; @InvocableVariable(required=true) public String prddetails; } @InvocableMethod(label='Evaluate Case Warranty' description='Parses product details JSON and updates Case status based on Asset and Entitlement validation') public static void evaluateWarranty(List<Request> requests) { if (requests == null || requests.isEmpty()) { return; } // Collect Case Ids for bulk processing Set<Id> caseIds = new Set<Id>(); for (Request r : requests) { if (r != null && r.caseId != null) { caseIds.add(r.caseId); } } if (caseIds.isEmpty()) { return; } // Query Cases with Account (Person Account model) Map<Id, Case> caseMap = new Map<Id, Case>([ SELECT Id, AccountId, ContactId, Status FROM Case WHERE Id IN :caseIds ]); // Parse JSON and prepare cases for update List<Case> casesToUpdate = new List<Case>(); Date today = Date.today(); for (Request request : requests) { if (request == null || request.caseId == null) { continue; } Case currentCase = caseMap.get(request.caseId); if (currentCase == null) { continue; } // Parse JSON to ProductDetails ProductDetails productInfo = parseProductDetails(request.prddetails); if (productInfo == null || String.isBlank(productInfo.SerialNumber)) { // Invalid JSON or missing serial number casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = 'Product not found')); continue; } // Determine customer account ID Id customerAccountId = getCustomerAccountId(currentCase, productInfo); if (customerAccountId == null) { casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = 'Product not found')); continue; } // Query Asset by Serial Number and Customer Account Asset matchingAsset = findAssetBySerialNumber(productInfo.SerialNumber, customerAccountId); if (matchingAsset == null) { casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = 'Product not found')); continue; } // Check warranty status String warrantyStatus = checkWarrantyStatus(matchingAsset, productInfo, today); casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = warrantyStatus)); } // Bulk update cases if (!casesToUpdate.isEmpty()) { try { update casesToUpdate; } catch (DmlException e) { System.debug('Error updating cases: ' + e.getMessage()); } } } // Parse JSON string to ProductDetails object private static ProductDetails parseProductDetails(String jsonString) { if (String.isBlank(jsonString)) { return null; } try { System.debug(jsonString); String cleanJsonString = jsonString.replace('\u00A0', ' '); //String jsonupdated = jsonString.replace('(Intel i5, 16GB RAM, 512GB SSD)', ''); jsonString.remove('(Intel i5, 16GB RAM, 512GB SSD)'); System.debug(cleanJsonString); return (ProductDetails) JSON.deserialize(cleanJsonString, ProductDetails.class); } catch (Exception e) { System.debug('Error parsing JSON: ' + e.getMessage()); return null; } } // Determine customer account ID from Case or ProductDetails // Assumes Person Account model where Case.AccountId is populated with Person Account private static Id getCustomerAccountId(Case currentCase, ProductDetails productInfo) { // Priority 1: Case AccountId (Person Account) // In Person Account model, this will be the Person Account ID if (currentCase.AccountId != null) { return currentCase.AccountId; } // Priority 2: Find Person Account by Customer Name from JSON if (!String.isBlank(productInfo.CustomerName)) { try { Account customerAccount = [ SELECT Id FROM Account WHERE Name = :productInfo.CustomerName AND IsPersonAccount = true LIMIT 1 ]; return customerAccount.Id; } catch (QueryException e) { System.debug('Person Account not found for customer: ' + productInfo.CustomerName); } } return null; } // Find Asset by Serial Number and Customer Account private static Asset findAssetBySerialNumber(String serialNumber, Id accountId) { try { return [ SELECT Id, AccountId, SerialNumber, Product2Id FROM Asset WHERE SerialNumber = :serialNumber AND AccountId = :accountId LIMIT 1 ]; } catch (QueryException e) { System.debug('Asset not found for serial number: ' + serialNumber); return null; } } // Check warranty status based on Entitlement private static String checkWarrantyStatus(Asset asset, ProductDetails productInfo, Date today) { // Query Entitlement for the asset's account List<Entitlement> entitlements = new List<Entitlement>(); try { entitlements = [ SELECT Id, StartDate, EndDate, AccountId FROM Entitlement WHERE AccountId = :asset.AccountId AND assetid = :asset.id // AND StartDate = :productInfo.WarrantyStartDate //AND EndDate = :productInfo.WarrantyEndDate LIMIT 1 ]; } catch (QueryException e) { System.debug('Error querying entitlements: ' + e.getMessage()); } // Check if matching entitlement exists and is still valid if (!entitlements.isEmpty()) { Entitlement entitlement = entitlements[0]; if (entitlement.EndDate != null && entitlement.EndDate >= today) { return 'In warranty'; } } return 'Out of warranty'; }}
***************
And now let us call this from the flow:
- Create Prompt Builder Template for file parsing
Create a new prompt template of type flex:
Keep the default on model selection and proceed with creating the prompt.
Make sure to use insert resource and select the related atatchments(combined attachments) to get the files for processing.
In the prompt ask the builder to retrieve the data in json format so that the flow can then process this.
Now let us preview this prompt builder action by giving a test case record.
For that either use emailtocase or manually create a case with specific attachments.
Laptop invoice:
Now, click on preview in the prmpt builder and enter the created case number:
This makes sure that the prompt is working as expected.
- Record Triggered Flow on Case creation
Now let us create a record triggered flow on case to call the prompt template for processing the attachments in the case.
Keep the criteria for flow execution simple so that it’s easy to test the setup.
-
Add an Action — Create an action named RetrieveWarrantyDetails to invoke the Prompt Builder template for processing the attached files.
-
Pass Parameters — Pass the Case record as a parameter to the action.
-
Store Response — Capture the response JSON text from the Prompt Builder in a flow variable (e.g., response).
- Invocable Apex Class Creation
To handle the JSON parsing and required validations, I have created an Invocable Apex Class (fully generated using GitHub Copilot).
This class accepts the JSON text and Case as input parameters, performs the necessary validations, and updates the product status accordingly.***************public with sharing class CaseWarrantyFlowHandler { // Inner class to represent the product details structure public class ProductDetails { public String CustomerName; public String ProductDescription; public String SerialNumber; public Date DateOfPurchase; public Date WarrantyStartDate; public Date WarrantyEndDate; } // Request wrapper class for Flow input public class Request { @InvocableVariable(required=true) public Id caseId; @InvocableVariable(required=true) public String prddetails; } @InvocableMethod(label='Evaluate Case Warranty' description='Parses product details JSON and updates Case status based on Asset and Entitlement validation') public static void evaluateWarranty(List<Request> requests) { if (requests == null || requests.isEmpty()) { return; } // Collect Case Ids for bulk processing Set<Id> caseIds = new Set<Id>(); for (Request r : requests) { if (r != null && r.caseId != null) { caseIds.add(r.caseId); } } if (caseIds.isEmpty()) { return; } // Query Cases with Account (Person Account model) Map<Id, Case> caseMap = new Map<Id, Case>([ SELECT Id, AccountId, ContactId, Status FROM Case WHERE Id IN :caseIds ]); // Parse JSON and prepare cases for update List<Case> casesToUpdate = new List<Case>(); Date today = Date.today(); for (Request request : requests) { if (request == null || request.caseId == null) { continue; } Case currentCase = caseMap.get(request.caseId); if (currentCase == null) { continue; } // Parse JSON to ProductDetails ProductDetails productInfo = parseProductDetails(request.prddetails); if (productInfo == null || String.isBlank(productInfo.SerialNumber)) { // Invalid JSON or missing serial number casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = 'Product not found')); continue; } // Determine customer account ID Id customerAccountId = getCustomerAccountId(currentCase, productInfo); if (customerAccountId == null) { casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = 'Product not found')); continue; } // Query Asset by Serial Number and Customer Account Asset matchingAsset = findAssetBySerialNumber(productInfo.SerialNumber, customerAccountId); if (matchingAsset == null) { casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = 'Product not found')); continue; } // Check warranty status String warrantyStatus = checkWarrantyStatus(matchingAsset, productInfo, today); casesToUpdate.add(new Case(Id = currentCase.Id, Product_status__c = warrantyStatus)); } // Bulk update cases if (!casesToUpdate.isEmpty()) { try { update casesToUpdate; } catch (DmlException e) { System.debug('Error updating cases: ' + e.getMessage()); } } } // Parse JSON string to ProductDetails object private static ProductDetails parseProductDetails(String jsonString) { if (String.isBlank(jsonString)) { return null; } try { System.debug(jsonString); String cleanJsonString = jsonString.replace('\u00A0', ' '); //String jsonupdated = jsonString.replace('(Intel i5, 16GB RAM, 512GB SSD)', ''); jsonString.remove('(Intel i5, 16GB RAM, 512GB SSD)'); System.debug(cleanJsonString); return (ProductDetails) JSON.deserialize(cleanJsonString, ProductDetails.class); } catch (Exception e) { System.debug('Error parsing JSON: ' + e.getMessage()); return null; } } // Determine customer account ID from Case or ProductDetails // Assumes Person Account model where Case.AccountId is populated with Person Account private static Id getCustomerAccountId(Case currentCase, ProductDetails productInfo) { // Priority 1: Case AccountId (Person Account) // In Person Account model, this will be the Person Account ID if (currentCase.AccountId != null) { return currentCase.AccountId; } // Priority 2: Find Person Account by Customer Name from JSON if (!String.isBlank(productInfo.CustomerName)) { try { Account customerAccount = [ SELECT Id FROM Account WHERE Name = :productInfo.CustomerName AND IsPersonAccount = true LIMIT 1 ]; return customerAccount.Id; } catch (QueryException e) { System.debug('Person Account not found for customer: ' + productInfo.CustomerName); } } return null; } // Find Asset by Serial Number and Customer Account private static Asset findAssetBySerialNumber(String serialNumber, Id accountId) { try { return [ SELECT Id, AccountId, SerialNumber, Product2Id FROM Asset WHERE SerialNumber = :serialNumber AND AccountId = :accountId LIMIT 1 ]; } catch (QueryException e) { System.debug('Asset not found for serial number: ' + serialNumber); return null; } } // Check warranty status based on Entitlement private static String checkWarrantyStatus(Asset asset, ProductDetails productInfo, Date today) { // Query Entitlement for the asset's account List<Entitlement> entitlements = new List<Entitlement>(); try { entitlements = [ SELECT Id, StartDate, EndDate, AccountId FROM Entitlement WHERE AccountId = :asset.AccountId AND assetid = :asset.id // AND StartDate = :productInfo.WarrantyStartDate //AND EndDate = :productInfo.WarrantyEndDate LIMIT 1 ]; } catch (QueryException e) { System.debug('Error querying entitlements: ' + e.getMessage()); } // Check if matching entitlement exists and is still valid if (!entitlements.isEmpty()) { Entitlement entitlement = entitlements[0]; if (entitlement.EndDate != null && entitlement.EndDate >= today) { return 'In warranty'; } } return 'Out of warranty'; }}
***************
And now let us call this from the flow:
Make sure to use insert resource and select the related atatchments(combined attachments) to get the files for processing.
This makes sure that the prompt is working as expected.
- Record Triggered Flow on Case creation
Keep the criteria for flow execution simple so that it’s easy to test the setup.
-
Add an Action — Create an action named
RetrieveWarrantyDetailsto invoke the Prompt Builder template for processing the attached files. -
Pass Parameters — Pass the Case record as a parameter to the action.
-
Store Response — Capture the response JSON text from the Prompt Builder in a flow variable (e.g.,
response).
- Invocable Apex Class Creation
This class accepts the JSON text and Case as input parameters, performs the necessary validations, and updates the product status accordingly.
Validation Output
We’re done! The file processing is now much simpler.
After creating a Case and saving it, you can observe that the Product Status field is automatically updated with the correct value.
We’re done! The file processing is now much simpler.
After creating a Case and saving it, you can observe that the Product Status field is automatically updated with the correct value.
Observations / Considerations
While experimenting with Prompt Builder multi-model processing, I noticed the following:
-
Model Performance:
It’s worth trying different models in Prompt Builder to compare responses.-
For simple files, OpenAI GPT-4 performs well.
-
For complex PDFs or images, Gemini 2.5 Flash provided more accurate and reliable results.
-
-
JSON Clean-Up:
Occasionally, the generated JSON may contain unwanted or special characters, which can cause parsing issues in the Apex class.
To address this, I added the following code snippet to clean up the JSON string: -
Access Issues:
At times, Prompt Builder displayed access-related issues when trying to retrieve Case records or associated files.

Comments
Post a Comment