Custom Providers
PRO Custom Providers are a Timelines Pro feature.
Custom providers allow you to create timeline items through custom Apex code by implementing the tln_TimelineItemProvider interface. This gives you complete flexibility to source timeline data from external systems, perform complex calculations, or aggregate data from multiple objects.
The tln_TimelineItemProvider Interface
The interface is simple but powerful:
global interface tln_TimelineItemProvider {
List<afp.tln_TimelineItem> getItems(Id recordId, Datetime minDate, Datetime maxDate, String ownerId);
}
Interface Parameters
- recordId: The ID of the record where the timeline is being displayed
- minDate: The earliest date for items to display (based on timeline date filter)
- maxDate: The latest date for items to display (based on timeline date filter)
- ownerId: The ID of the user viewing the timeline (used for "My Items" filtering)
tln_TimelineItem Class
global with sharing class tln_TimelineItem implements Comparable {
// Constructor
global tln_TimelineItem();
// Getter methods
global String getDescription();
global String getTitle();
global String getSubtitle();
global String getRecordId();
global String getIconName();
global Boolean getRich();
global String getLink();
global Datetime getLastOpenedDate();
global Boolean getHasAttachments();
global Boolean getHighPriority();
global List<tln_TimelineAction> getActions();
global Date getDate();
global Datetime getDateTime();
// Setter methods
global void setDescription(String descr);
global void setTitle(String title);
global void setSubtitle(String subtitle);
global void setRecordId(String recordId);
global void setIconName(String iconName);
global void setRich(Boolean rich);
global void setLink(String link);
global void setLastOpenedDate(Datetime lastOpenedDate);
global void setHasAttachments(Boolean hasAttachments);
global void setHighPriority(Boolean highPriority);
global void addAction(tln_TimelineAction action);
global void setDate(Date value);
global void setDate(Datetime value);
// Comparable implementation
global Integer compareTo(Object compareTo);
}
tln_TimelineAction Class
global with sharing class tln_TimelineAction implements Comparable {
// Constructor
global tln_TimelineAction();
// Getter methods
global String getLabel();
global String getScreenFlowApiName();
global String getAutoLaunchedFlowApiName();
global String getApexCallableName();
global String getLightningEventType();
global Decimal getOrder();
// Setter methods
global void setLabel(String label);
global void setScreenFlowApiName(String screenFlowApiName);
global void setAutoLaunchedFlowApiName(String autoLaunchedFlowApiName);
global void setApexCallableName(String apexCallableName);
global void setLightningEventType(String lightningEventType);
global void setOrder(Decimal order);
// Comparable implementation
global Integer compareTo(Object compareTo);
}
Creating a Custom Provider
Step 1: Implement the Interface
Create an Apex class that implements afp.tln_TimelineItemProvider:
global with sharing class MyCustomProvider implements afp.tln_TimelineItemProvider {
global List<afp.tln_TimelineItem> getItems(Id recordId, Datetime minDate, Datetime maxDate, String ownerId) {
List<afp.tln_TimelineItem> items = new List<afp.tln_TimelineItem>();
// Your custom logic here
// Query external systems, aggregate data, etc.
return items;
}
}
Step 2: Build Timeline Items
Create tln_TimelineItem objects using the available setter methods:
afp.tln_TimelineItem item = new afp.tln_TimelineItem();
// Required fields
item.setRecordId(recordId); // ID of the related record
item.setTitle('My Custom Timeline Item'); // Main title
item.setDate(Datetime.now()); // Timeline date/time
// Optional display fields
item.setSubtitle('Additional context'); // Subtitle text
item.setDescription('Detailed description'); // Main description
item.setRich(true); // Enable HTML in description
item.setIconName('standard:event'); // Salesforce icon name
item.setLink(recordId); // Navigation link or Id of the record to link to
item.setLastOpenedDate(Datetime.now()); // Last viewed timestamp
item.setHasAttachments(true); // Show attachment indicator
item.setHighPriority(true); // Show priority indicator
Step 3: Add Actions (Optional)
Timeline items can include interactive actions:
// Create an action
afp.tln_TimelineAction action = new afp.tln_TimelineAction();
action.setLabel('Custom Action');
action.setApexCallableName('MyCallableClass:executeAction');
item.addAction(action);
// Other action types
action.setScreenflowApiName('My_Screen_Flow'); // Launch screen flow
action.setAutoLaunchedflowApiName('My_Auto_Flow'); // Launch auto flow
action.setLightningEventType('CustomEventType'); // Publish Lightning event
Complete Example
Here's a complete example that creates timeline items from external API data:
global with sharing class ExternalApiProvider implements afp.tln_TimelineItemProvider {
global List<afp.tln_TimelineItem> getItems(Id recordId, Datetime minDate, Datetime maxDate, String ownerId) {
List<afp.tln_TimelineItem> items = new List<afp.tln_TimelineItem>();
try {
// Get account email for external API lookup
Account acc = [SELECT Email__c FROM Account WHERE Id = :recordId LIMIT 1];
if (String.isBlank(acc.Email__c)) {
return items;
}
// Call external API (implement your HTTP callout)
List<ExternalEvent> externalEvents = callExternalAPI(acc.Email__c, minDate, maxDate);
// Convert external events to timeline items
for (ExternalEvent event : externalEvents) {
afp.tln_TimelineItem item = new afp.tln_TimelineItem();
item.setRecordId(recordId);
item.setTitle(event.eventName);
item.setSubtitle('External System Event');
item.setDescription(buildDescription(event));
item.setRich(true);
item.setDate(event.eventDate);
item.setIconName('standard:event');
item.setLink(event.externalUrl);
// Add custom action
afp.tln_TimelineAction syncAction = new afp.tln_TimelineAction();
syncAction.setLabel('Sync to Salesforce');
syncAction.setApexCallableName('ExternalEventSync:syncEvent');
item.addAction(syncAction);
items.add(item);
}
} catch (Exception e) {
// Handle errors gracefully
System.debug('Error in ExternalApiProvider: ' + e.getMessage());
// Optionally create an error timeline item
afp.tln_TimelineItem errorItem = new afp.tln_TimelineItem();
errorItem.setRecordId(recordId);
errorItem.setTitle('External API Error');
errorItem.setDescription('Failed to load external events: ' + e.getMessage());
errorItem.setDate(Datetime.now());
errorItem.setIconName('utility:error');
items.add(errorItem);
}
return items;
}
private List<ExternalEvent> callExternalAPI(String email, Datetime minDate, Datetime maxDate) {
// Implement your external API call here
// Return parsed external events
return new List<ExternalEvent>();
}
private String buildDescription(ExternalEvent event) {
return '<p><strong>Event Type:</strong> ' +
event.eventType +
'</p>' +
'<p><strong>Details:</strong> ' +
event.details +
'</p>' +
'<p><a href="' +
event.externalUrl +
'" target="_blank">View in External System</a></p>';
}
// Inner class for external event data
public class ExternalEvent {
public String eventName;
public String eventType;
public String details;
public Datetime eventDate;
public String externalUrl;
}
}
Configuring Custom Providers
Setup Configuration
- Navigate to the Timeline Setup page
- Create a new timeline setup or edit an existing one
- Add a new setup item
- Select "Custom Apex Class" as the provision type
- Choose your provider class from the dropdown
When configuring the setup item through Salesforce Setup > custom metadata instead, the Item Object field should use the format:
apex:namespace.YourProviderClassName
Best Practices
Performance Considerations
- Limit data volume: Return only essential timeline items within the date range
- Use efficient queries: Optimize any SOQL queries in your provider
- Handle timeouts: External API calls should have appropriate timeout handling
- Cache when possible: Consider caching external data to improve performance
Error Handling
global List<afp.tln_TimelineItem> getItems(Id recordId, Datetime minDate, Datetime maxDate, String ownerId) {
List<afp.tln_TimelineItem> items = new List<afp.tln_TimelineItem>();
try {
// Your provider logic
} catch (CalloutException e) {
// Handle API timeouts/failures
System.debug('Callout failed: ' + e.getMessage());
// create an error timeline item
afp.tln_TimelineItem errorItem = new afp.tln_TimelineItem();
errorItem.setRecordId(recordId);
errorItem.setTitle('External API Error');
errorItem.setDescription('Failed to load external events: ' + e.getMessage());
errorItem.setDate(Datetime.now());
errorItem.setIconName('utility:error');
items.add(errorItem);
} catch (Exception e) {
// Handle other errors
System.debug('Provider error: ' + e.getMessage());
// create an error timeline item
afp.tln_TimelineItem errorItem = new afp.tln_TimelineItem();
errorItem.setRecordId(recordId);
errorItem.setTitle('My Item Error');
errorItem.setDescription('Failed to load timeline items: ' + e.getMessage() + '\n' + e.getStackTraceString());
errorItem.setDate(Datetime.now());
errorItem.setIconName('utility:error');
items.add(errorItem);
}
return items; // Always return a list, even if empty
}
Security
- Use
with sharing: Ensure your provider class respects sharing rules - Validate parameters: Check that recordId and other parameters are valid
- Sanitize external data: Clean any data from external sources before display
- Permission checks: Verify user permissions for sensitive operations
Date Filtering
Respect the provided date parameters to ensure good performance:
// If possible filter the items in SOQL as much as possible
String queryString =
'SELECT ... ' +
'FROM MyObject__c ' +
'WHERE (MyDateField__c >= :minDate AND MyDateField__c <= :maxDate) ' +
(String.isNotBlank(ownerId) ? 'AND OwnerId = :ownerId ' : '') +
'AND ... SomeOtherField__c = :recordId ... ' +
'ORDER BY MyDateField__c ASC ' +
'LIMIT 100 ';
for (MyObject__c record: Database.query(queryString)) {
// Create the timeline item
}
// When not using SOQL, filter your data source by the provided date range
if (myItem.eventDate >= minDate && myItem.eventDate <= maxDate) {
// Include myItem
}
Owner Filtering
When ownerId is provided, filter items to show only those relevant to that user:
// Filter in SOQL as shown in the example above, or
// Example: only show items where the user is the owner or participant
if (String.isBlank(ownerId) || myItem.OwnerId == ownerId) {
// Include myItem
}
Testing Custom Providers
Create test classes to ensure your provider works correctly:
@isTest
public class ExternalApiProviderTest {
@isTest
static void testGetItems() {
// Setup test data
Account testAccount = new Account(Name = 'Test Account', Email__c = 'test@example.com');
insert testAccount;
// Test the provider
Test.startTest();
ExternalApiProvider provider = new ExternalApiProvider();
List<afp.tln_TimelineItem> items = provider.getItems(
testAccount.Id,
Datetime.now().addDays(-30),
Datetime.now().addDays(30),
UserInfo.getUserId()
);
Test.stopTest();
// Verify results
Assert.isNotNull(items, 'Items list should not be null');
// Add more specific assertions based on your logic
}
}
Troubleshooting
Provider Not Found Error
If you see "Invalid provider class" errors:
- Verify the class name is spelled correctly in the setup
- Ensure the class implements
afp.tln_TimelineItemProvider - Check that the class is global and active
- Confirm proper namespace prefix if applicable
Empty Timeline
If your provider returns no items:
- Check debug logs for any exceptions in your provider
- Verify the date range parameters are being respected
- Ensure your provider logic matches the test data
- Test your provider directly in anonymous Apex
Performance Issues
If the timeline loads slowly:
- Review your provider's query efficiency
- Consider limiting the number of items returned
- Add appropriate indexes to custom objects
- Optimize external API calls and add caching