The scenario
CarePoint Health runs a network of clinics. Their number-one inbound call is billing: "Why did I get this invoice?" "Didn't my insurance cover that?" "What do I actually owe?" The answers exist — but scattered across the billing system, the payer/claims feed and the patient portal, none of which the service agent can see.
CarePoint already unified this data in Data 360 (the pipeline from Part 1): billing invoices, claim statuses and payment history all resolve to one unified patient profile. Now they want their Agentforce service agent to answer billing questions in the portal chat — from the unified record, live, mid-conversation. Not from last night's sync. Not from the Contact record alone.
This is a different problem from RAG
We've covered grounding agents in unstructured knowledge — PDFs, articles, chunked and vector-indexed — in the Agentforce Data Library walkthrough. That's the right tool when the question is "what's the refund policy?"
CarePoint's question is different: "what does this specific patient owe?" The answer isn't a passage to retrieve — it's structured data to look up: rows, amounts, dates, joined across systems. Grounding on structured Data 360 profiles is its own discipline, with its own tools. Use both in the same agent; don't confuse them.
| Question type | Grounding tool | Example |
|---|---|---|
| Policy / how-to (unstructured) | Data Library, search index, retriever | "Is a telehealth visit billed differently?" |
| This customer's facts (structured) | Data graph, direct query, Apex action | "What's my balance on invoice 4417?" |
Real time vs batch: pick per question, not per project
Structured grounding has two speeds, and choosing wrong costs either money or accuracy:
- Real-time (data graphs): a data graph is a precomputed, denormalized view of a unified profile and its related DMOs, materialized as a single document. Because it's precalculated, reads return in near real time — this is the shape you want inside a live conversation.
- On-demand SQL (Query API / Apex): flexible, joins anything, but each call is metered compute with real latency. Right for questions a data graph doesn't already answer.
- Batch (Calculated Insights): precomputed metrics like "balance overdue > 60 days", refreshed on schedule (Part 2). Perfect for flags and scores; wrong for "what did I just pay?", where stale answers destroy trust.
Build step 1 — the data graph
CarePoint creates a data graph in Data 360 (Data Graphs → New):
- Primary object: Unified Individual — the patient.
- Related objects:
Billing_Invoice__dlm(open and recent invoices),Claim__dlm(status, payer),Payment__dlm(last 12 months). - Fields: only what the agent needs — invoice number, amount due, due date, claim status. Not the diagnosis codes. Grounding scope is a security decision, and the smallest useful graph wins.
Once built, the graph maintains itself as underlying DMOs change, and any consumer — prompt templates, agents, the API — reads the same fresh document.
Build step 2 — ground the prompt template
In Prompt Builder, CarePoint creates a flex template for the agent's billing topic. The instruction block is where a healthcare agent is kept honest:
You are a billing assistant for CarePoint Health.
Answer ONLY from the billing facts provided in this prompt.
If the facts don't contain the answer, say so and offer to
connect a human agent. Never speculate about insurance
coverage decisions.
Patient's question: {!$Input:patient_question}
The billing facts themselves come from a grounding resource added through the template's resource picker. Data graphs can be attached directly as a grounding source, or — for full control over the shape of what the model sees — through an Apex data provider that reads the graph and returns a Prompt string:
public with sharing class PatientBillingFacts {
public class Request {
@InvocableVariable(required=true)
public String unifiedPatientId;
}
public class Response {
@InvocableVariable
public String Prompt;
}
@InvocableMethod(capabilityType='FlexTemplate://CarePoint_Billing_Answers')
public static List<Response> getFacts(List<Request> requests) {
ConnectApi.CdpQueryInput input = new ConnectApi.CdpQueryInput();
input.sql =
'SELECT invoice_number__c, amount_due__c, due_date__c, claim_status__c ' +
'FROM Billing_Invoice__dlm ' +
'WHERE unified_patient_id__c = \'' +
String.escapeSingleQuotes(requests[0].unifiedPatientId) + '\' ' +
'AND status__c = \'Open\' ORDER BY due_date__c';
ConnectApi.CdpQueryOutputV2 result =
ConnectApi.CdpQuery.queryAnsiSqlV2(input);
Integer numCol = result.metadata.get('invoice_number__c').placeInOrder;
Integer amtCol = result.metadata.get('amount_due__c').placeInOrder;
Integer dueCol = result.metadata.get('due_date__c').placeInOrder;
Integer stsCol = result.metadata.get('claim_status__c').placeInOrder;
List<String> lines = new List<String>();
for (ConnectApi.CdpQueryV2Row row : result.data) {
lines.add('Invoice ' + row.rowData[numCol] +
': amount due ' + row.rowData[amtCol] +
', due ' + row.rowData[dueCol] +
', claim status: ' + row.rowData[stsCol]);
}
Response res = new Response();
res.Prompt = lines.isEmpty()
? 'No open invoices for this patient.'
: String.join(lines, '\n');
return new List<Response>{ res };
}
}
The Einstein Trust Layer sits in the middle of every resolution: sensitive fields can be masked before the prompt reaches the model, and grounding data never becomes training data. Note what the Apex controls — exactly which fields, in exactly what phrasing, reach the model. That precision is why the Apex-provider route is worth the extra class in regulated industries.
Build step 3 — an Apex action for the awkward questions
Some questions don't fit a precomputed graph — "how much have I paid toward my deductible this year?" needs an aggregate with a date filter. For those, CarePoint gives the agent a custom action: an @InvocableMethod that queries Data 360 directly and returns a typed answer.
public with sharing class PatientPaymentsAction {
public class Request {
@InvocableVariable(required=true label='Unified patient Id')
public String unifiedPatientId;
}
public class Response {
@InvocableVariable(label='Payments summary')
public String summary;
}
@InvocableMethod(
label='Get Patient Payments This Year'
description='Sums this year\'s payments for a unified patient profile from Data 360.')
public static List<Response> run(List<Request> requests) {
List<Response> out = new List<Response>();
for (Request req : requests) {
ConnectApi.CdpQueryInput input = new ConnectApi.CdpQueryInput();
input.sql =
'SELECT SUM(amount__c) AS paid_ytd__c ' +
'FROM Payment__dlm ' +
'WHERE unified_patient_id__c = \'' +
String.escapeSingleQuotes(req.unifiedPatientId) + '\' ' +
'AND payment_date__c >= DATE_TRUNC(\'year\', CURRENT_DATE)';
ConnectApi.CdpQueryOutputV2 result =
ConnectApi.CdpQuery.queryAnsiSqlV2(input);
Response res = new Response();
if (result.data.isEmpty()) {
res.summary = 'No payments found this year.';
} else {
Integer col = result.metadata.get('paid_ytd__c').placeInOrder;
Object paid = result.data[0].rowData[col];
res.summary = 'Payments this calendar year: ' +
(paid == null ? '0' : String.valueOf(paid));
}
out.add(res);
}
return out;
}
}
Attach it to the billing topic in Agentforce Builder like any invocable action. The agent decides when to call it; your Apex decides what it's allowed to know. If the action should ever write data back, add the validation-and-confirmation guardrails from our governed agent actions build — read paths and write paths deserve different levels of paranoia.
The conversation, end to end
A patient types: "I got an invoice for $180 but I thought insurance covered my visit." What happens:
- The agent classifies the request into the billing topic.
- The prompt template resolves the data graph for this patient: invoice 4417, $180, claim status "processed — patient responsibility: deductible".
- The model answers from those facts: the claim was processed, $180 applied to the deductible, and here's the payment link.
- The patient asks "how much of my deductible have I met?" — the agent calls
PatientPaymentsActionand answers with the live number.
Every fact in that exchange came from the unified profile. The same conversation grounded only on CRM would have ended at "I'll create a case for our billing team" — which is exactly the call volume CarePoint was trying to deflect. For the same pattern in an employee-facing channel, see our Agentforce-in-Slack build, which grounds on live CRM through Data 360.
What you learned
- Structured grounding (this customer's facts) and unstructured RAG (policies, articles) are different problems with different tools — use both, deliberately.
- Data graphs precompute a unified profile into one fast-reading document — the default for in-conversation grounding.
- Prompt templates ground on data graph fields with Trust Layer controls; instructions enforce "answer only from facts".
- Custom
@InvocableMethodactions withConnectApi.CdpQueryhandle the questions no precomputed shape covers. - Scope grounding like a security review: smallest graph, least-privileged agent user.
Next in the series: reacting to Data 360 changes from Apex and Flow — when the data should come to you instead. The full series lives on the Data 360 track page.
Sources: Salesforce Help — Data Graphs · Grounding with Data Graphs · Salesforce Developers — Ground a Prompt with Data 360 (workshop)