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.
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.
- Query
IdandDeveloperNamefromReport, and keep the Id of the row you meant to run. - Call
Reports.ReportManager.runReportwith the Id for a fast run. Passtruewhen you want detail rows, because that argument isincludeDetails. - Call
runAsyncReportwith the same kind of arguments when the run may take a while. You get aReportInstanceback, not the rows. - Call
getReportInstancewith the instance id when you are ready to read. CheckgetStatusbeforegetReportResults. A status of Error means the instance finished unsuccessfully, and that value is not an Apex exception. - Call
getFactMapon the results. On a tabular report, the key isT!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.
- Call
describeReport, thengetReportMetadataon the describe result. - Take the filter you want from
getReportFiltersand callsetValue. The official sample sets a date on the first filter, runs the report, sets a different date, and runs it again. - Read the grand total from the
T!Tfact. Cast that fact when you need aggregates, callgetAggregates, thengetLabelon 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.
- POST to
/analytics/reportswithreportMetadata. A name and areportTypeare enough to create the report. Other properties are optional. - Read
reportExtendedMetadata,reportMetadata, andreportTypeMetadataon the response so you can confirm the type you asked for. - 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.sheetand leave Content-Type asapplication/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/limitsshows 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.
-
ENTERTAINMENT5 months agoSophia Wootten Turns the Miss Georgia Crown Toward Cleft Care
-
NEWS5 months agoGeorgia Beach Bacteria List Leaves Swim Areas Open
-
NEWS1 year agoThe iPhone 16 Pro Flipkart Cut Did Not Last
-
BUSINESS2 months agoUltraTech’s Ultravolt Launch Puts Cable Multiples on Trial
-
NEWS10 months agoIXI Autofocus Glasses Still Lack a European CE Mark
-
NEWS4 months agoLittle St. Simons Island Hatches Loggerheads at 72.4 Percent
-
NEWS2 months agoMicrosoft Prints Azure’s $29.42 Billion Quarter and Shrinks the Map
-
NEWS1 year agoCapyfriends Cafe Routes Helen Tourists to Cleveland Capybaras
