Andy in the Cloud

From BBC Basic to Force.com and beyond…


16 Comments

Supercharging Salesforce Report Subscriptions with Apex and Flow

In the Spring’15 release Salesforce provided the ability to subscribe to reports. This is done through a new Schedule button shown below. Users can schedule reports with criteria based on the report contents. When the platform runs the report, the criteria gets evaluated and if met will result in notifications sent to the user. The usual Email and Chatter options are available, as well Mobile Notifications for Salesforce1 mobile users, pretty cool!

ReportSubscribe

It is the last notification option that caught my interest. Described simply as Execute Custom Action. This is in fact the gateway to many more possibilities, as it is essentially a peace of any Apex code. When the report is run and the criteria is met the platform will call your Apex code, a bit like an Apex Trigger but for Reports. Given the capabilities of Apex and what users can do with Salesforce Reports, thats not just pretty cool, that’s super cool!

Subscription

Once the user saves the subscription the platform creates a scheduled entry that can be seen and deleted by administrators under the All Scheduled Jobs page under Setup, classified as Reporting Notification scheduled job type. The job will then run under the user context of the user that setup the subscription. Note that each user can only setup up to 5 report subscriptions presently.

Creating a Custom Action with Apex

In order for your Apex class to appear in the above UI for selection you must implement a platform provided Apex interface. The Reports.NotificationAction Apex interface is pretty simple, with just one execute method.

public with sharing class MyReportNotification 
	implements Reports.NotificationAction {

	public void execute(Reports.NotificationActionContext context) {
	    // Context parameter contains report instance and criteria
	    Reports.ReportResults results = context.getReportInstance().getReportResults();
	    // In the above subscription case this is the 'Record Count'
	    System.debug(context.getThresholdInformation().getEvaluatedConditions()[0].getValue();
	    // You can also access the report definition!
	    System.debug(results.getReportMetadata().getName());
	}
}

If your familiar with the Analytics Apex API, you’ll soon feel quite at home with the information provided by the context parameter. The context parameter passes in an instance of the report itself, as in the records of the report. As well as a reminder of the criteria behind the subscription (as entered in the UI above) and the aggregate values that caused the criteria to be met. Also the report definition (its metadata), useful if you wanted to write a generic handler for example.

What about Clicks not Coders?

Now i love Apex coding, but i also know that this platform has not got where it has from just providing a programming language. Having a declarative means to deliver solutions rapidly with more pervasive skills is the corner stone of what makes the platform so successful. Tools like Process Builder and Visual Flow are a great example of this.

So you may think Salesforce have missed an opportunity here, by not providing a means to use for example an Autolaunched Flow as a Report subscription. I personally think so, hence i’ve raised this Idea Exchange idea here, please up vote if you agree. Meanwhile, all is not lost!  As i’ve previously blogged there is a means to invoke Flow’s from Apex. So this got me wondering if Apex could be the glue between Salesforce Report Notifications and Flow?

Here is the result of a basic experiment of calling a Flow from an Apex Report Notification…

public with sharing class MyReportNotification 
	implements Reports.NotificationAction {

	public void execute(Reports.NotificationActionContext context) {
		// Context parameter contains report instance and criteria
		Reports.ReportResults results = context.getReportInstance().getReportResults();
		// Construct some parameters to pass to the Flow
		Map<String, Object> params = new Map<String, Object>();
		params.put('ReportName', results.getReportMetadata().getName());
		params.put('RecordCount', context.getThresholdInformation().getEvaluatedConditions()[0].getValue());
		// Call the Flow with the parameters
		Flow.Interview.MyReportNotificationFlow reportNotificationFlow = 
			new Flow.Interview.MyReportNotificationFlow(params);
		reportNotificationFlow.start();		 
	}
}

Implementation Note: The only drawback so far i can see is sadly you cannot pass the whole context parameter “as is” into Flow, as its variable types are quite limited, to make this more generic you would have to be quite inventive as to how you pass information.

The following shows the Autolaunched Flow called in the above example. It’s pretty simple but achieves something the other notification options cannot which is to create Task. If you want to know more see my blog here.

ReportNotificationFlow

This results in a Task being created for the user as shown in the screenshot below. There is certainly a great potential for this type of approach, as it allows for clicks not coders to stretch their declarative skills without having to revisit the Apex code each time a new requirement or change is required.

TaskCreatedByReportFlowNotification

Running, Testing and Debugging

SubscribeSaveAndRunIf you look closely at the UI above you’ll see the button Save & Run Now. As the name suggests this will save the subscription details and run the report immediately. If the criteria is met your Apex class will be called immediately also. This is obviously pretty useful as the most granular scheduling period is on the hour.

ScheduledSubscriptionReportWhen i did schedule the subscription as normal users would. I found oddly no evidence of it in the Apex Jobs page, despite a schedule appearing on the All Scheduled Jobs page. Its pretty obvious the platform runs these async, so i would have expected some evidence of this…

During my testing i found the Debug Logs output as expected (in both execution modes above). Though an important note is any exceptions thrown got swallowed when run from the UI, so always check your debug log if things are not working as expected.

Warning Possible Platform Bug: I did find that if my Apex code threw an exception when i used the UI to run my code it deleted any prior schedules for the report subscription. I had to delete and recreate the subscription to get the schedule entry back. I’ll investigate this a bit further and raise a case with Salesforce.

Writing an Apex Test for Report Notification Actions

Writing Apex Tests for the above is little different than the usual approach, mainly owing to the fact that the Analytics API requires that Apex tests leverage the SeeAllData attribute. This is clearly less than ideal, but is indeed documented as such in the formal Salesforce documentation.

In order to emulate the platform calling your execute method, you’ll need to mock the context parameter, fortunately it seems unlike some system types the context parameter type can be constructed, nice!

@IsTest
private class MyReportNotificationTest {
	
	@IsTest(SeeAllData=true)
	private static void testMyReportNotification() {
	
		// Get an instance of the report
		Report report = [SELECT Id FROM Report WHERE DeveloperName = 'ClosedWon'];
		Test.startTest();
		Reports.ReportInstance reportInstance =
          Reports.ReportManager.runAsyncReport(report.Id, true);
		Test.stopTest();

		// Emulate the platform calling the custom action passing criteria that was met
		MyReportNotification myReportNotification = new MyReportNotification();		
		Reports.NotificationActionContext context = 
			new Reports.NotificationActionContext(
				reportInstance, 
				new Reports.ThresholdInformation( 
					new List<Reports.EvaluatedCondition> {
							/* new Reports.EvaluatedCondition(
								'RecordCount', 
								'Record Count', Double.valueOf(0), Double.valueOf(1), 
								Reports.EvaluatedConditionOperator.GREATER_THAN) */ }));
		myReportNotification.execute(context);

		// Assert accordingly...
		// ...
	}
}

Implementation Note: At time of writing, i found that i could not create a mock instance of the Reports.EvaluatedCondition type without receiving a compilation error claiming the constructor was not found (hence why its commented out). I’ll update this blog if i find out why. This issue will limit the testing of any criteria / threshold references in your code you make.

Summary

I love how Salesforce are opening up more of the platform to support Apex callbacks like this. It adds much more flexibility for developers to really extend the platform and not just solutions on top of it. In this case it also provided some flexibility for us to plug any gaps in the platforms current feature set. Salesforce please keep it up!