Opportunity: calls, transcripts and emails
Overview
This recipe retrieves the last 180 days of call activity and emails for a given Opportunity — including transcripts for any calls that have them — using the full programmatic access pipeline: BuildScopeForRecord → GatherPhoneNumbersInScope → GetCallSegments → GetTranscripts, with GetEmailMessageIds branching from the scope in parallel.
The scope is built once from the Opportunity and reused for both retrieval paths. Call segments are matched by phone number (via the gathered set from step 2); transcripts are fetched per page of segments using toGetTranscriptsRequest(); email message IDs are matched by record association (directly from the scope — emails relate to records, not phone numbers).
The communication scope for an Opportunity automatically includes the Opportunity's Account and all Contacts on that Account. Because the phone number set is resolved once in step 2, each subsequent call segment page is a cheap SOQL query against that fixed set rather than re-running scope resolution on every iteration.
Code
Id opportunityId = '006...'; // your Opportunity record ID
Datetime cutoff = Datetime.now().addDays(-180);
// Step 1 — Resolve which records are in scope for this Opportunity.
// For an Opportunity this includes the Account and all Contacts on that Account.
// WithoutSharing is appropriate for batch/system contexts.
callcoreio.BuildScopeForRecord.Response scope =
callcoreio.BuildScopeForRecord.construct(callcoreio.SharingRule.WithoutSharing)
.execute(new callcoreio.BuildScopeForRecord.Request(opportunityId));
// The scope is inspectable — call scope.getScope().toList() to see which records
// (grouped by SObject type) will be queried before committing to the fetch.
// Step 2 — Gather phone numbers from the in-scope records.
callcoreio.GatherPhoneNumbersInScope.Response phoneNumbers =
callcoreio.GatherPhoneNumbersInScope.construct(callcoreio.SharingRule.WithoutSharing)
.execute(scope.toGatherPhoneNumbersRequest());
// Resolve the Source__c IDs for your CallCore workspace subdomain.
// Supply them explicitly — in a batch/system context the running user may not
// drive the RestrictSources strategy correctly.
Set<Id> sourceIds = callcoreio.ServiceProvider.getSourceLookup()
.getSourceIdsBySubdomain('acme');
if (sourceIds.isEmpty()) return; // subdomain not registered — nothing to query
// Step 3 — Page through call segments using the gathered phone numbers.
// bypassRunningUserPermissions() skips canViewSomeCalls() — an automation user
// won't hold CallCore user permissions. fromSources() replaces the RestrictSources
// strategy with the explicitly resolved source IDs.
callcoreio.GetCallSegments.IHandler segHandler =
callcoreio.GetCallSegments.construct(callcoreio.SharingRule.WithoutSharing);
callcoreio.GetTranscripts.IHandler txHandler = callcoreio.GetTranscripts.construct();
List<callcoreio.PhoneCallSegment> allSegments = new List<callcoreio.PhoneCallSegment>();
List<callcoreio.PhoneCallTranscript> allTranscripts = new List<callcoreio.PhoneCallTranscript>();
callcoreio.GetCallSegments.Request segReq =
phoneNumbers.toGetCallSegmentsRequest()
.occurredAfter(cutoff)
.take(50)
.bypassRunningUserPermissions()
.fromSources(sourceIds);
callcoreio.GetCallSegments.Response segResp;
do {
segResp = segHandler.execute(segReq);
allSegments.addAll(segResp.segments);
// Step 4 — Fetch transcripts for this page's segments with Available status.
// toGetTranscriptsRequest() scopes the fetch to segments where a transcript exists.
callcoreio.GetTranscripts.Response txResp =
txHandler.execute(segResp.toGetTranscriptsRequest());
allTranscripts.addAll(txResp.transcripts);
if (segResp.moreAvailable) segReq.forNextPage(segResp);
} while (segResp.moreAvailable);
// Step 3b — Get email message IDs from the scope.
// Emails are associated with records, not phone numbers — so this feeds from scope
// directly via toGetEmailMessageIdsRequest(), not from phoneNumbers.
callcoreio.GetEmailMessageIds.IHandler emailHandler =
callcoreio.GetEmailMessageIds.construct(callcoreio.SharingRule.WithoutSharing);
Set<Id> allEmailIds = new Set<Id>();
callcoreio.GetEmailMessageIds.Request emailReq =
scope.toGetEmailMessageIdsRequest()
.sentAfter(cutoff)
.take(50);
callcoreio.GetEmailMessageIds.Response emailResp;
do {
emailResp = emailHandler.execute(emailReq);
allEmailIds.addAll(emailResp.emailMessageIds);
if (emailResp.moreAvailable) emailReq.forNextPage(emailResp);
} while (emailResp.moreAvailable);
// Retrieve EmailMessage details using SOQL
List<EmailMessage> emails = new List<EmailMessage>();
if (!allEmailIds.isEmpty()) {
emails = [
SELECT Id, Subject, MessageDate, FromAddress, ToAddress, TextBody
FROM EmailMessage
WHERE Id IN :allEmailIds
];
}
Notes
Scope inspection — before committing to a fetch, call scope.getScope().toList() to see which records (grouped by SObject type) will be included. This is useful when a human confirmation step is needed — show the operator the scope, get approval, then proceed to phone gathering and data retrieval.
Narrowing — call scope.excluding(recordId) to remove a specific record before gathering phone numbers or querying emails. Call phoneNumbers.excluding(phoneNumber) to remove a specific number before querying call segments. Call segResp.toGetTranscriptsRequest().excluding(segmentId) to skip a specific segment's transcript. Neither operation affects the other path — excluding a record from scope does not remove numbers that were already gathered if you saved the phone numbers response first.
Transcripts per page — GetTranscripts is called inside the paging loop rather than after it. This avoids accumulating a large set of segment IDs in memory and fetches transcripts while the segment data is still local. Only segments with Available status produce a transcript — Pending, Failed, and NotApplicable segments appear in allSegments but not in allTranscripts. Use seg.transcriptionStatus to distinguish these cases when building your output.
Governor limits — each iteration of the paging loops issues internal SOQL queries. Keep take at 50 (the maximum) to minimise iterations. For very high-volume accounts, prefer calling this from a Queueable or a dedicated Batch class where you have a full query budget.
Scope behaviour — the Opportunity scope delegates to its Account, which in turn includes all Contacts on that Account. If the Account has many contacts with many phone numbers, the scope can be wide. Implement a custom BuildCommunicationScopeForRecord strategy for the Opportunity SObject type and register it via ExtensibilityStrategy__mdt to narrow it.