Andy in the Cloud

From BBC Basic to Force.com and beyond…

Supercharging Salesforce Report Subscriptions with Apex and Flow

17 Comments

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!

 

 

17 thoughts on “Supercharging Salesforce Report Subscriptions with Apex and Flow

  1. Thanks for the excellent post.

    Do you know if there is a way to create an instance of a flow dynamically?

    Here is the scenario.

    I would like write 1 class that my admins can tie to any report. The admins will set up metadata in the system (custom settings or custom metadata types) which will define the report name (or ID) and the flow that has to run with the results of the report. Based on the report, they would like to run different flows. I would like to read the metadata and dynamically instantiate the flow and run it with the parameters.

    • Thanks! I am actually in the middle of building a packaged tool for this very purpose, it will be my second blog this month and the solution will be open source.

  2. I actually found a way to accomplish the defaults for your tests.

    I ran into a similar situation where I was unable to create a mock instance to my object.

    Here is the question and answer for it here: http://salesforce.stackexchange.com/questions/102338/test-class-for-quickaction-quickactiondefaultshandler

    Once I remembered the ability to change a Map to an instance (like serializedUntyped), I knew it could work the same way.

    I tried this out in Anonymous Apex and I am able to construct what you need.


    List<Map> reportingConditionAsObjectList = new List<Map>
    {
    new Map
    {
    'aggregateName' => 'RecordCount',
    'aggregateLabel' => 'Record Count',
    'compareTo' => Double.valueOf(0),
    'Value' => Double.valueOf(1),
    'operator' => Reports.EvaluatedConditionOperator.GREATER_THAN
    }
    };

    List defaultConditions =
    (List)JSON.deserialize(JSON.serialize(reportingConditionAsObjectList), List.class);

  3. I actually had a similar situation with the following question I posted: http://salesforce.stackexchange.com/questions/102338/test-class-for-quickaction-quickactiondefaultshandler

    Turns out, you can use JSON.serialize to create instance variables that are difficult to mock.

    Once I realized that I can change a Map into an instance of my choosing, it actually becomes pretty straight-forward.

    So the following code should suffice for you test class:


    List<Map> reportingConditionAsObjectList = new List<Map>
    {
    new Map
    {
    'aggregateName' => 'RecordCount',
    'aggregateLabel' => 'Record Count',
    'compareTo' => Double.valueOf(0),
    'Value' => Double.valueOf(1),
    'operator' => Reports.EvaluatedConditionOperator.GREATER_THAN
    }
    };

    List defaultConditions =
    (List)JSON.deserialize(JSON.serialize(reportingConditionAsObjectList), List.class);

  4. Pingback: Salesforce: Automate Adding Contacts to Campaign via Report Subscription | Doug Ayers

  5. Andrew,
    Is there a way to send an email to the user instead of creating a Task? I’m using a custom Action because the Subect and the body of the message needs to be customized, I created an apex class that implements a notification Action, just like you did and I pass the parameters to the Flow. Once I’m on the Flow, I don’t know how to tell the Flow to grab the parameters I’m trying to pass.
    What does your TaskReminder looks like behind the scene in your Flow?

  6. Hi Andrew. Thanks for the post. I’m going to create a great tool for Admins to provide the ability to schedule record updates. Workflow only allows Admins to easily schedule one action but not a repeating action.

    My idea is to have Admins use the report engine to specify the criteria.
    Place the record id in the first column and a datetime field to update in the second column.
    Schedule a generic Apex script which will add a timestamp to all of the designated fields. This will allow their process builders and flows to fire 🙂

    I will probably need to make them batchable to avoid flow errors (101 soql errors). But I think that could be such a useful tips for Admins looking to schedule

  7. Hi Andrew. Thanks for the post. I’m going to create a great tool for Admins to provide the ability to schedule record updates. Workflow only allows Admins to easily schedule one action but not a repeating action.

    My idea is to have Admins use the report engine to specify the criteria.
    Place the record id in the first column and a datetime field to update in the second column.
    Schedule a generic Apex script which will add a timestamp to all of the designated fields. This will allow their process builders and flows to fire 🙂

    I will probably need to make them batchable to avoid flow errors (101 soql errors). But I think that could be such a useful tips for Admins looking to schedule

  8. Pingback: An API junkies perspective on Spring’17 | Andy in the Cloud

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s