The prompt template dilemma
Retrieving unstructured data is one of the first things that trips developers up in Agentforce. You build a Prompt Template, pick your Input Object, and expect the associated records to come along with it. Then you go looking for ContentDocument, ContentVersion, or the legacy Note and Attachment objects, and the Prompt Builder resource picker does not offer them.
Prompt Builder is optimized for querying relational data through the standard Data Graph layer. Notes and Attachments (and the newer Files/ContentDocument structure) sit behind polymorphic relationships and system-level joins, so the template editor never presents them as "Related Lists". Feeding that content to your LLM takes a detour.
The architecture limitation: why Prompt Builder misses files
Prompt Builder relies on the Einstein Trust Layer to fetch context. When you use the "Template" feature, it generates a SOQL query behind the scenes for the fields your template defines. ContentDocumentLink, the object that connects an Account or Case to a file, is a junction object. A plain lookup field such as Contact.AccountId is easy for the prompt preview tool to follow. A junction is not, and the Prompt Builder UI does not traverse one natively.
So if your goal is to summarize a Case using the latest PDF attached to it, clicking "Insert Resource" and hunting for "Related Notes" gets you nowhere. You have to intervene at the data retrieval layer.
Strategy 1: using Apex-defined resources
The dependable fix is an Apex-defined Prompt Resource. You write a class that handles the ContentDocumentLink query, extracts the text content, and passes it to the prompt.
Start with an Invocable method that fetches the content:
public class PromptContentRetriever {
@InvocableMethod(label='Get Latest Attachment Text' description='Fetches text from latest file')
public static List<String> getAttachmentContent(List<Id> recordIds) {
List<String> results = new List<String>();
for (Id recordId : recordIds) {
// Fetch the most recent document link
ContentDocumentLink link = [SELECT ContentDocument.LatestPublishedVersion.VersionData
FROM ContentDocumentLink
WHERE LinkedEntityId = :recordId
ORDER BY SystemModStamp DESC LIMIT 1];
if (link != null) {
results.add(link.ContentDocument.LatestPublishedVersion.VersionData.toString());
} else {
results.add('No attachments found.');
}
}
return results;
}
}
Once it is deployed, open Prompt Builder, select Add Resource, choose Apex, and pick your PromptContentRetriever. Drag the resource into your template body. The LLM now receives the raw string of the attachment content directly in its context window.
Strategy 2: Data Cloud and unstructured data
At enterprise scale, writing Apex for every file turns into maintenance nobody wants to own. Data Cloud is the better home for it. Instead of querying local Salesforce objects, ingest the ContentVersion records into Data Cloud.
- Data Stream: create a Data Stream for the
ContentVersionobject. - Data model mapping: map the
VersionDatato a text-based attribute in the Data Model. - Search index: enable the Data Cloud Vector Database (if you are using RAG) or a standard lookup.
- Prompt Builder integration: use the Data Cloud resource type in your prompt template.
Once the files are indexed as unstructured text, the Agentforce prompt can run a vector search across thousands of them instead of being restricted to whichever attachment happens to be "latest".
Implementing a "safe" fallback
Never assume an attachment exists. Build a fallback condition into your prompt template so the LLM does not hallucinate when nothing comes back.
In your prompt template configuration, structure your instruction as follows:
"Review the following context provided from the Case record.
Case Details: {{Case.Subject}} / {{Case.Description}}
{IF: {!AttachmentResource} != 'No attachments found.'} Additional File Context: {!AttachmentResource} {ELSE} No file attachments were available for analysis. Proceed using only Case Details. {ENDIF}"
Leave a Comment