Report JSON
How a Folio report is written down as JSON, key by key, for options the Editor does not set and for reports generated by code.
Overview
Every report is stored as one JSON document in the Config JSON field of its Folio Definition record. The Editor writes it, and most reports never need anything else. Edit it by hand when you want:
- Presentation the Editor does not offer yet: coloring cells by rule, coloring column headers, fixed column widths, limiting which columns readers can sort, and PDF page sizing. See Display.
- Reports built by code, including reports whose data comes from somewhere other than a live read. See Supplying the data yourself.
To edit it, click Go to Definition on the report, then Edit on the record, and change Config JSON. The Editor keeps keys it does not show, so options you add by hand survive later edits in the Editor.
Nothing in the JSON is trusted. Every object and field name is checked against your org when the report runs, values are never pasted into a query, and the report reads data as the person viewing it. A field that does not exist, or that the reader cannot see, stops the report with an error naming it rather than showing anything it should not.
A report looks like this:
{
"schemaVersion": 1,
"format": "summary",
"primaryObject": "Opportunity",
"columns": [
{ "id": "c_name", "field": "Name", "label": "Opportunity", "dataType": "string" },
{ "id": "c_amt", "field": "Amount", "label": "Amount", "dataType": "currency", "aggregate": "SUM", "format": { "decimals": 0 } }
],
"groupRows": [
{ "id": "g_industry", "field": "Account.Industry", "label": "Industry", "dataType": "picklist", "direction": "ASC" }
],
"filters": {
"conditions": [ { "id": "1", "field": "IsWon", "operator": "equals", "value": true } ]
},
"chart": { "type": "bar", "measureRef": "c_amt", "title": "Won Revenue by Industry" }
}
Top level
| Key | Required | |
|---|---|---|
schemaVersion | Yes | Currently 1. |
format | Yes | tabular, summary, matrix or scorecard. |
primaryObject | Yes | API name of the object the report is on, such as Opportunity. |
columns | Yes | The fields shown. See Columns. |
groupRows | Summary, matrix, scorecard | What to group by. See Groupings. |
groupColumns | Matrix | What runs across the top of a matrix. |
filters | No | Which records to include. See Filters. |
sortBy | No | Default order. See Sort. |
rowLimit | No | How many records a summary lists under its groups. Defaults to, and cannot exceed, 2,000. Subtotals and totals count every record regardless, and a tabular report ignores it and pages through everything. |
chart | No | Summary and matrix. See Chart. |
display | No | Presentation. See Display. |
tools | No | Which reader tools are allowed. See Tools. |
A key Folio does not know is ignored.
Columns
{ "id": "c_amt", "field": "Amount", "label": "Amount", "dataType": "currency", "aggregate": "SUM", "format": { "decimals": 0, "prefix": "$" } }
| Key | |
|---|---|
id | Your own identifier, unique within the report. Sort, chart and display settings refer to columns by it. |
field | The field's API name. A lookup path reaches a related record: Account.Name, Account.Owner.Name. |
label | The column heading. |
dataType | How to format values: string, textarea, int, double, currency, percent, boolean, date, datetime, time, picklist, multipicklist, reference, id, email, phone or url. The real type is looked up in your org for anything that matters; this only affects display. |
aggregate | Optional. SUM, AVG, MIN, MAX, COUNT or COUNT_DISTINCT. A column with one is a measure: it fills subtotals, totals, matrix cells and charts. |
format | Optional number display: decimals, currencyCode (such as "USD"), prefix, suffix. |
Counting records is a measure on Id: { "id": "m_count", "field": "Id", "label": "Records", "aggregate": "COUNT", "dataType": "int" }.
On a matrix, only the first measure is used. It fills every cell, and columns without an aggregate are ignored.
Scorecard measures
On a scorecard, every column is a measure with a source: a related object whose records are gathered up for each row.
{
"id": "m_won", "label": "Won Revenue", "aggregate": "SUM", "dataType": "currency",
"source": {
"object": "Opportunity",
"joinPath": "AccountId",
"field": "Amount",
"filter": { "conditions": [ { "id": "1", "field": "IsWon", "operator": "equals", "value": true } ] }
}
}
| Key | |
|---|---|
source.object | The related object. |
source.joinPath | The lookup on that object pointing at the report's object. |
source.field | The field to summarize, or Id to count records. |
source.filter | Optional. conditions and logic as in Filters, on the related object's fields. |
joinPath has to point at the report's own object. A scorecard cannot have groupColumns.
Groupings
groupRows and groupColumns are lists of:
{ "id": "g_month", "field": "CloseDate", "label": "Month", "dataType": "date", "bucket": "month", "direction": "ASC" }
| Key | |
|---|---|
id, field, label, dataType | As for columns. |
direction | ASC or DESC: the order of the groups. |
bucket | Optional, date and date/time fields only: day, month, quarter or year, labeled 2026-06-19, 2026-06, 2026-Q2 and 2026. Calendar periods only, evaluated in GMT. |
displayField | Scorecard only. The field that labels each row. |
Only the first grouping in each list is used. Further entries are kept but have no effect.
A scorecard's row grouping is always on Id, one row per record, with displayField naming the label:
"groupRows": [ { "id": "g_account", "field": "Id", "displayField": "Name", "label": "Account", "dataType": "id", "direction": "ASC" } ]
Filters
"filters": {
"conditions": [
{ "id": "1", "field": "StageName", "operator": "in", "value": ["Prospecting", "Qualification"] },
{ "id": "2", "field": "Amount", "operator": "greaterThan", "value": 10000 },
{ "id": "3", "field": "Account.Industry", "operator": "equals", "value": "Education" }
],
"logic": "1 AND (2 OR 3)",
"dateFilter": { "field": "CloseDate", "range": "THIS_FISCAL_YEAR" }
}
operator | value |
|---|---|
equals, notEquals | A single value. For a checkbox, true or false. |
greaterThan, lessThan, greaterOrEqual, lessOrEqual | A single value. Dates as "2026-06-30". |
contains, notContains, startsWith | Text. |
in, notIn | A list of values. |
includes, excludes | A list of values, for multi-select picklists. |
isNull, isNotNull | None. |
logic combines conditions by their position in the list, counting from 1, with AND, OR, NOT and parentheses. NOT binds tightest, then AND, then OR. Leave it out and every condition must match. An expression that refers to a condition that is not there, or uses any other word, stops the report with an error.
dateFilter is applied on top of the conditions and outside logic. Its range is one of:
TODAY,YESTERDAY,TOMORROWTHIS_WEEK,LAST_WEEK,NEXT_WEEKTHIS_MONTH,LAST_MONTH,NEXT_MONTHTHIS_QUARTER,LAST_QUARTER,NEXT_QUARTERTHIS_YEAR,LAST_YEAR,NEXT_YEARTHIS_FISCAL_QUARTER,LAST_FISCAL_QUARTER,NEXT_FISCAL_QUARTERTHIS_FISCAL_YEAR,LAST_FISCAL_YEAR,NEXT_FISCAL_YEARLAST_N_DAYS:30orNEXT_N_DAYS:30, with any whole numberCUSTOM, withstartDateandendDateas"2026-01-01"
The Editor offers a shorter list, and keeps any of these you set by hand.
Sort
"sortBy": [ { "ref": "c_close", "direction": "DESC" }, { "ref": "c_name", "direction": "ASC" } ]
ref is a column or grouping id. Up to three are used, in order.
A matrix ignores sortBy; its order comes from its groupings' direction. A scorecard uses only the first entry, and only when it names a measure: rows are then ordered by that measure, with empty values last. Otherwise a scorecard follows its row grouping's direction.
Chart
"chart": { "type": "pie", "measureRef": "c_amt", "title": "Won Revenue by Industry" }
| Key | |
|---|---|
type | Summary: bar, horizontalBar, line, pie, doughnut. Matrix: bar (grouped), stackedBar, line. |
measureRef | The id of a column with an aggregate. Without one, there is no chart. |
title | Optional. Without it, the chart is named after its measure. |
A summary charts its grouping and a matrix charts its row grouping, with the column grouping as the series. The Editor also writes a dimensionRef naming the grouping; it is not read.
An unknown type, or one the format does not offer, draws as bar. So do pie, doughnut and stackedBar on a measure other than SUM or COUNT, since only those add up to a whole.
Display
Everything in display is optional, and anything left out keeps its default.
"display": {
"theme": { "--folio-brand": "#1F5C99" },
"header": { "fields": [ { "label": "Owner", "value": "Development Team" }, { "label": "Period", "value": "FY 2026" } ] },
"grid": { "cellPadding": "4px 10px", "cellBorderColor": "#D0D7E2", "columnWidths": { "c_notes": "220px" } },
"headerStyles": { "c_amt": { "background": "#E6F4EA", "color": "#1B5E20" } },
"conditionalFormats": [
{ "columns": ["c_status"], "equals": "At Risk", "style": { "background": "#FBE9C8", "color": "#8A5A00" } },
{ "columns": ["c_amt"], "when": { "op": "greaterThan", "value": 50000 }, "style": { "background": "#D6F0DE" } }
],
"sort": { "columns": ["c_name", "c_amt"] },
"hideMeta": true,
"pdf": { "fit": "width" }
}
| Key | Effect | In the Editor |
|---|---|---|
theme | --folio-brand sets the report's brand color. | Brand color |
header.fields | Label and value pairs under the title. An empty value shows as a dash. | Header Strip |
grid.cellPadding | Cell padding, as CSS. The default is 8px 12px; the Editor's Compact is 4px 10px. | Density |
grid.cellBorderColor | Draws cell borders in this color. | Cell border color |
grid.freezeFirstColumn | true keeps the first column in view on a wide summary or scorecard. | Freeze the first column |
grid.columnWidths | Column id to a fixed width, such as "220px". Long text in those columns wraps instead of widening them. Summary and scorecard only. | Not yet |
headerStyles | Column id to a background and color for that header. | Not yet |
conditionalFormats | Colors data cells by rule. See below. | Not yet |
sort | Tabular only. { "enabled": false } turns header sorting off; { "columns": [...] } allows it on those column ids only. | Not yet |
hideMeta | true hides the line under the title. | Hide the record-count / format subline |
pdf | PDF page size. See below. | Not yet |
grid.cellPadding and grid.cellBorderColor apply to every format except matrix.
Any other grid.cellPadding becomes 4px 10px the next time the report is saved in the Editor, since those two are all it offers.
Conditional formats
A list of rules, checked in order. The first rule that matches a cell colors it.
| Key | |
|---|---|
columns | Column ids the rule applies to. Leave it out for every column. |
equals | Matches a cell whose value is exactly this. |
contains | Matches a cell whose value contains this text. |
when | A number comparison: op is greaterThan, greaterOrEqual, lessThan, lessOrEqual, equals or notEquals, against value. Dollar signs, commas and percent signs are ignored, so it works on formatted text too. |
style | background and color. |
Rules color the cells of records, not subtotal or total rows. Header styles and conditional formats both carry into the Excel (Formatted) and PDF exports.
PDF page size
| Key | |
|---|---|
fit | auto, the default: standard Letter when the table fits, a wider page when it does not. width: always a page as wide as the table. letter: always Letter, squeezing columns to fit. |
pageWidth, pageHeight | Exact page size in points, overriding fit. pageHeight can be "auto" for one continuous page. |
Tools
Which reader tools the report allows. Leave tools out to allow everything.
"tools": {
"export": { "enabled": true, "modes": ["pdf", "csv"] },
"filter": { "enabled": false },
"chart": { "enabled": true }
}
Once tools is present, it is a list of what is allowed: a tool shows only if its key is there and not false. modes narrows the export formats to xlsx, xlsxFormatted, pdf, csv and json; leave it out for all of them.
Where the report is placed can narrow this further but never widen it, apart from the admin views. See The Utility Bar setting.
Supplying the data yourself
A report does not have to read Salesforce. Put rows in the definition's Data JSON field and the report shows them instead, with the same formatting, grouping, subtotals, totals and charts. This suits figures your own code computes, or data held outside Salesforce.
{ "rows": [
{ "g_region": "North", "c_program": "After School", "c_served": 412 },
{ "g_region": "North", "c_program": "Summer Camp", "c_served": 188 },
{ "g_region": "South", "c_program": "After School", "c_served": 305 }
] }
Each row is keyed by column and grouping id:
| Format | One row per |
|---|---|
| Tabular | Record, with a value for each column. |
| Summary | Record, with its group's value under the grouping's id. |
| Matrix | Cell: the row group's value, the column group's value, and the measure. |
| Scorecard | Report row: the label under the grouping's id, and a value for each measure. |
Folio builds the groups, subtotals, totals and matrix itself; supply only the rows. A missing value shows as a dash.
Because nothing is read from Salesforce:
- Give every column a
labelanddataType, since there is no field to look them up from.fieldandprimaryObjectare not used. filtersdo not apply. Put only the rows you want in the data.- Readers get no Filter tool, since there is nothing to rerun.
- The whole field holds 131,072 characters, which suits summaries and scorecards better than long lists.
Code that creates these records needs create and edit access to Folio Definitions, which Flourish Folio: Admin grants. Set Name, Unique Name, Type (Report), Status (Published), Config JSON and Data JSON. Version, the version key and the live version are filled in for you.