Connect with us

TECHNOLOGY

How to Report in Apex: A Practical Reports API Guide

How to report in Apex starts with Reports.ReportManager: run the saved report sync or async, read the fact map, and keep sync rows off the 50,000-row governor.

Published

on

If the report already exists in Salesforce, how to report in Apex means running that saved report and reading the result, not rebuilding the groupings with SOQL. You keep the formulas, the buckets, and the sharing the report builder already applied.

Inside a class, the call is Reports.ReportManager. Outside Apex, or when you need to create, clone, delete, or download the report, you use the Reports and Dashboards REST API. This page covers which call fits the job, how a one-time filter works, how to read the fact map, and the limits that stop the run.

Which Call Runs a Saved Report?

Salesforce gives you two supported ways to run a report, and each one fits a different place in your code. The Apex classes run a report that already has an Id, in the same transaction as the rest of the class. The REST API is an HTTP call, so it fits a web app, a phone app, or any caller that is not allowed to use the Apex report methods.

Job Call that fits Leave this one out
Run a saved report in a class runReport or runAsyncReport A trigger
Change filters for one run Pass ReportMetadata into the run method, or POST metadata to the REST run resource Saving the report when only this run should change
Create, clone, or delete a report POST, PATCH, or DELETE on /analytics/reports ReportManager, which has no create method
Download Excel Reports REST API with an Excel Accept header A normal JSON run
List reports opened lately GET the REST report list Apex, which has no method for recently run reports

People use a run to copy values onto a custom object, feed a chart, or refresh a dashboard they built on top of the same data. Describe, run, and fetch an async instance all live on ReportManager. Creating, cloning, deleting, and listing recently viewed reports are REST jobs. Report calls are not allowed in Apex triggers, so a trigger has to hand the work to another class.

When to Run a Report Sync or Async

Once the call belongs in a class, the next choice is whether this transaction must wait for the rows. Run the report synchronously when you expect it to finish quickly and the class needs the rows before the transaction ends. For a long report, the async methods are the better fit, because a long run is less likely to hit a timeout and you can read the stored results again later.

Question Sync run Async run
When it fits The report returns quickly and you need rows in this transaction The report may run long, or you will read it again
REST timeout The 2-minute overall API timeout still applies That 2-minute timeout does not apply
How long results last Only for this call A rolling 24-hour period
Apex row governor Report rows count toward the 50,000 SOQL rows in the transaction Report rows are not added to that 50,000
Most rows in the response The first 2,000 The first 2,000

If the class also queries accounts, opportunities, or users, a sync report spends the same row budget those queries use. Call runAsyncReport, keep the instance id, and read the results on a later pass. Asynchronous report calls are not allowed in batch Apex, so put a long run in a class that is not a batch job.

How to Report in Apex From a Saved Report

With sync or async chosen, the class still needs an Id and a method. Query the Report object by DeveloperName, which is the lookup in Salesforce’s own filter sample for a report named Closed_Sales_This_Quarter. The run methods then take that Id.

  1. Query Id and DeveloperName from Report, and keep the Id of the row you meant to run.
  2. Call Reports.ReportManager.runReport with the Id for a fast run. Pass true when you want detail rows, because that argument is includeDetails.
  3. Call runAsyncReport with the same kind of arguments when the run may take a while. You get a ReportInstance back, not the rows.
  4. Call getReportInstance with the instance id when you are ready to read. Check getStatus before getReportResults. A status of Error means the instance finished unsuccessfully, and that value is not an Apex exception.
  5. Call getFactMap on the results. On a tabular report, the key is T!T.

Each of runReport and runAsyncReport has four forms: the Id alone, the Id plus metadata, the Id plus includeDetails, and all three together. Pass metadata when this run needs a different filter. Pass true for includeDetails when you need the record rows and not only the summaries. The API returns the same number of records your user would see for that report in Salesforce.

Three other static methods matter once the run works. describeReport returns the report, the report type, and extended metadata for a tabular, summary, or matrix report. getReportInstances lists async instances for one report, up to the stored set for that report. getDatatypeFilterOperatorMap lists the operators each field type allows, which is what you check before you invent a filter the field will reject.

How to Filter a Salesforce Report on the Fly

A filter you pass at run time can leave the saved report unchanged. In Apex, change the ReportMetadata and hand it to runReport or runAsyncReport. On the REST API, POST that metadata to the sync or async run resource. A POST to the query resource goes further and returns data without creating a report or changing one you already saved.

  1. Call describeReport, then getReportMetadata on the describe result.
  2. Take the filter you want from getReportFilters and call setValue. The official sample sets a date on the first filter, runs the report, sets a different date, and runs it again.
  3. Read the grand total from the T!T fact. Cast that fact when you need aggregates, call getAggregates, then getLabel on the aggregate you care about.

You can add up to 20 custom field filters when you run a report. Cross filters, standard report filters, and filtering by row limit are unavailable when you filter through the API. Before you set a value, the describe metadata can tell you whether that column is filterable, and which values the column already allows.

How to Read a Salesforce Report Fact Map

The numbers you filtered come back inside a fact map, and the key you read depends on the report format. Each group is numbered from 0. Detail cells show up when you ran with includeDetails set to true. hasDetailRows is true when the results are at record level and false when they are summary only.

Format Key What that key selects
Tabular T!T The grand total, and the detail rows when you asked for them
Summary 0!T The first group on the first grouping level
Summary 1!T The second group on the first grouping level
Summary 0_1!T The first top group and the second group under it
Matrix 0!0 The first row group crossed with the first column group
Matrix T!T The grand total of the matrix

Count the rows on the T!T fact when you need to know whether any records came back. The detail-rows flag only tells you that the run included record-level data, and developers still meet runs where that flag reads true while the fact has no data cells. A matrix key such as 0_0!0_0 walks one level deeper on both axes: the first child of the first row group, crossed with the first child of the first column group.

On ReportResults, getAllData tells you whether the whole result came back. True means you have all of the report results. False means you have the same number of rows a run in Salesforce would show that user, which is the moment to tighten filters rather than to expect another page of rows. Metadata can also name the format MULTI_BLOCK, available in API version 43.0 and later. The key guide walks through tabular, summary, and matrix reports, so read a joined report’s metadata before you assume one of these keys.

REST API Tasks You Cannot Do in Apex

Those keys are what a class gets back from ReportManager. A few jobs never appear on that class, and those jobs are HTTP calls. The Reports and Dashboards REST API is available for any org that has API access enabled, and you need an authenticated session using OAuth. Requests and responses are JSON.

You build that URL the way the Reports and Dashboards REST API guide lays it out, from the My Domain login URL, then the version, then the resource. The previous-versions list includes API version 68.0 for the Winter ’27 guide, so a reports list on that version looks like https://MyDomainName.my.salesforce.com/services/data/v68.0/analytics/reports. You can read the My Domain login URL on the My Domain page in Setup.

Hands-on samples in the guide still use older versions, and they still show the path shape you copy. A sync sample GETs /services/data/v35.0/analytics/reports/00OR0000000K2UeMAK?includeDetails=true and returns summary data with details. An async sample POSTs an empty body to that report’s /instances path, takes the instance handle from the response, and GETs /instances/0LGR00000000He3OAE for the rows. Use the version your integration is allowed to call, and keep the resource path.

How to Create or Clone a Report

Creating a report is a POST, not a run. You send reportMetadata to the reports resource, and the saved report is what later run calls will execute.

  1. POST to /analytics/reports with reportMetadata. A name and a reportType are enough to create the report. Other properties are optional.
  2. Read reportExtendedMetadata, reportMetadata, and reportTypeMetadata on the response so you can confirm the type you asked for.
  3. To clone, POST to the report list with a cloneId. To change a saved report, PATCH the report resource. To delete, send DELETE, which moves the report to the Recycle Bin.

How to Download a Report as Excel

A formatted Excel file is a different call from a JSON run, and it asks for products the run call never mentions.

THE DOWNLOAD LICENSE

  • The run call: Any org with API enabled can use the Reports and Dashboards REST API once OAuth succeeds.
  • The Excel call: The Analytics Download API also needs a CRM Analytics license, Slack for Salesforce, and CRM Analytics for Slack. Turn those products on with the steps to enable Slack for Salesforce integrations.
  • The header: Set Accept to application/vnd.openxmlformats-officedocument.spreadsheetml.sheet and leave Content-Type as application/json.

You can POST once to run and download, or POST with Accept set to application/json and then GET the file. The file name arrives on the Content-Disposition header. This endpoint is a poor fit for several huge exports at once, so do not run more than one large report export at a time. Each user can also make 3 concurrent requests to download Analytics assets, which is a separate cap from that one-at-a-time warning.

Dashboard Refreshes and Report Notifications

Dashboard calls and notification calls have their own caps, and they do not spend your report-run allowance. From the dashboard resources you can list recently used dashboards, read metadata and data, check status, and refresh.

  • Dashboard refreshes: An org can request up to 200 dashboard refreshes an hour.
  • Dashboard results: An org can request results for up to 5,000 dashboards an hour.
  • Report subscriptions: Each user can subscribe to up to 5 reports.
  • Analytics notifications: Each user can create up to 5 Analytics notifications.
  • Notification version: The notifications API starts in version 38.0, and a GET to /analytics/notifications/limits shows how many more that user can create.

A deleted notification cannot be recovered. Put refreshes on a schedule that stays inside the refresh cap, because a busy org can spend that hourly allowance while report runs still have room.

Report Run Limits for Apex and the REST API

Every create, run, and download above still sits under the same report caps. Inside a class, these caps are the Apex report limits Salesforce publishes. Over HTTP, the same run numbers are on the Reports REST API limits page.

SHARED REPORT RUN CAPS

  • Columns: The API processes a report with up to 100 fields selected as columns.
  • Recent reports: A list of up to 200 recently viewed reports can be returned, and Apex has no method for that list.
  • Sync pace: An org can request up to 500 synchronous runs an hour, with up to 20 synchronous runs at a time.
  • Async pace: An org can request up to 1,200 asynchronous requests an hour, with up to 200 requests at a time for async results.
  • Stored instances: A list of up to 2,000 async instances of one report can be returned, sorted by the time the run was requested.
  • Rows: The API returns up to the first 2,000 report rows, so narrow the report with filters when that slice is too wide.
  • Custom filters: You can add up to 20 custom field filters on a run.

Limits that apply to a report built in the report builder also apply when you run it through the API. The same is true for a dashboard built in the dashboard builder. Async results from either path stay available for the rolling 24-hour period in the sync and async table above.

How to Test a Report Run in Apex

Tests are where a report run parts company with a normal SOQL test. Report code still needs coverage. The reporting methods run as the logged-in user, with only the access that user already has.

In a test, report runs ignore SeeAllData whether the annotation is true or false, and there is no way to turn that off. Results can include rows the test did not insert. Rows you insert in the test are not guaranteed to show up either. Filter to the records you mean to check, and assert on that filtered value.

An async report in a test runs only after Test.stopTest. That call flushes async work for the current test method only. A helper that later calls getReportInstance does not run the report again. Check getStatus before getReportResults, and treat a status of Error as a finished failure.

  • Filter the assert: Check a filtered aggregate, not how many rows the org already had.
  • Wait for stopTest: Start the async run, call Test.stopTest, then read the instance in that same test method.
  • Status before rows: Read results only after you have checked getStatus.

If an automated process user runs the report on a standard or custom object from an Apex test class, only the required custom fields come back. Non-required custom fields are absent, so write the assertion against a required field or run as a different user. The sample in the Apex guide assumes a saved report whose developer name is Closed_Sales_This_Quarter, creates an opportunity, uses it as the filter, and asserts on what that filter returns.

Frequently Asked Questions

What Content Type Does a Reports API POST Need?

Set Content-Type to application/json on every POST. If you skip that content type, the call can come back with results you did not ask for. An Excel download keeps Content-Type as application/json and uses the Accept header when you want a spreadsheet instead of JSON.

Can a Web App Use the Same Report Data?

Yes. A web or mobile app, inside or outside Salesforce, can call the REST API and use the report that the report builder already defined. You might post a Chatter snapshot of top-performing reps each quarter, which is the example the REST overview uses, and that call still has to send an OAuth session.

Which Reports Support Historical Tracking?

Historical tracking reports are only supported as matrix reports. Subscriptions are not supported for those historical tracking reports. If the report you have is tabular or summary and it depends on historical tracking, rebuild it as a matrix report before you try to run it here.

What Does Salesforce Return for a Retired API Version?

Salesforce keeps each API version for at least three years from its first release and gives at least one year of notice before support ends. The Connect REST end-of-life table published August 13, 2026 lists versions 41.0 through 68.0 as supported. A call that uses a retired version returns HTTP 410 GONE.

Ship the class only after one run as the user who will own the job. Read the T!T row count and confirm it is the slice you filtered for. If that sync run plus the other queries in the transaction would pass the 50,000 SOQL rows, switch the call to runAsyncReport and read the instance after getStatus shows the run finished rather than Error.

Harry is the editor of RIVERDALE STANDARD, an independent title he owns and runs. He has spent ten years in journalism, first as a reporter and then as an editor, and that time taught him that how a publication handles its mistakes says more than how it handles its scoops. The corrections policy here is public. When an error is found, the article is updated, a dated note at the top explains what changed and why, and nothing is quietly rewritten. Readers who spot a problem are credited if they want to be. The same care goes into getting things right the first time: stories are built from filings, statements, transcripts and datasets, quotes are checked against the recording, and every figure is confirmed against its source before publication. Harry writes for an international readership across ten sections, from news, business and technology through science and sports to entertainment, lifestyle, travel, auto and gaming. Reader mail is answered personally at support@riverdalestandard.com.

Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Trending