How can I get a report to return more than 2,000 rows using the "ReportResults" class in the "Reports" namespace?

The documentation is fairly explicit here:

The API returns up to the first 2,000 report rows. You can narrow results using filters.

You'll need to execute your report multiple times, using filters that you can guarantee will return fewer than 2000 rows (such as by using date ranges, report run #1 uses THIS_QUARTER, run #2 uses LAST_QUARTER, or you use first letter of name starts with A on run #1, then B on run #2)


In the end, I ended up getting around this limitation using the following strategy:

Create an auto number on the object you're trying to report on (make sure to check off the option to back-populate existing records).

Add the auto number to the report and sort by that number (can be in descending or ascending order, you will need to handle that later on). Add a report filter using that number and set it to "greater than blank" so it includes all records.

Create a while loop (or for loop, depending on your use case. I used a while loop) and within the while loop, run a report with metadata. Then, dynamically update the filter so that at each incremental run, the report returns different results, effectively getting around the 2k limitation. This worked on 9k+ records synchronously.

Here's a code abstract of how I did it.

public class CreateCampaignMembers{    

    public static void CreateCampaignMembersFromReport(id campaignId, String userSetStatus,id reportId){

        //This will be the initial filter for the report until it runs a second time.
        //the second time and on, the filter gets dynamically assigned the autonumuber related to the bottommost id on the report.
        String filterNumber = '0';

        //initialize numOfRow at 1. When it gets reassigned to 0, stop the while loop
        id firstconid;
        Integer numOfRows = 1;
        Integer loopCount = 0;

        //you do your setup here. i'm setting up a campaign member list.

        //create a set of campaign memebers. To be populated in the for loop with contacts. Sets removes possibility of dupes.
        Set<CampaignMember> campaignmembers = new Set<CampaignMember>();
        //create list for insert at the end
        List<CampaignMember> campaignMemberList = new List<CampaignMember>();
        Boolean manualBreak = false;

        //break loop when report stops returning results, or when told to break.
        while(numOfRows>0 && !manualBreak){
            loopCount++;                

            // Get the report metadata in order to create dynamic filter (since reports run with a maximum of 2,000 return results)
            Reports.ReportDescribeResult describe = Reports.ReportManager.describeReport(reportId);
            Reports.ReportMetadata reportMd = describe.getReportMetadata();           
            Reports.ReportFilter filter = reportMd.getReportFilters()[0];
            filter.setValue(String.valueOf(filterNumber));                              

            // Run a report synchronously
            Reports.reportResults ActiveISOContacts = Reports.ReportManager.runReport(reportId, reportMd, true);
            ActiveISOContacts.getAllData();

            // Get the fact map from the report results
            Reports.ReportFactWithDetails factDetails = 
            (Reports.ReportFactWithDetails)ActiveISOContacts.getFactMap().get('T!T');

            //create a list of report rows and populate it with the result rows from fact map
            List<Reports.ReportDetailRow> reportRows = factDetails.getRows();

            numOfRows = reportRows.size();
            System.debug('Value of numOfRows: ' + numOfRows);
            //loop through each report detail row and create a campaign member

            //loop throgh row by row the get the data
            for(integer count = 0; count<numOfRows;count++){                        

                List<Reports.ReportDataCell> datacells = reportRows[count].getDataCells();
                Reports.ReportDataCell datacell = datacells[0];
                id conid = (id)datacell.getValue();

                //make sure loop doesn't enter infinite loop
                if(count==0 && loopCount>1 && firstconid==conid){
                    manualBreak = true;
                    break;
                }

                //set conid to firstconid for reference in the next loop
                if(count==0){                       
                    firstconid = conid;                     
                }

                CampaignMember campaignmember = new CampaignMember(CampaignId=campaignId,ContactId=conid,Status=userSetStatus);
                campaignmembers.add(campaignmember);

                **//HERE's WHERE YOU SET THE FILTER TO DYNAMICALLY CHANGE (only get it when you're in the last row of the report results)**
                if(count==numOfRows-1){
                   filterNumber= [Select Record_Number_Id__c FROM Contact WHERE id= :conid].Record_Number_Id__c;
                }

            }
        }
        campaignMemberList.addall(campaignmembers);
        Database.insert(campaignMemberList,false);
    }       
}