Report Data & Aggregations

Read a report's combined records, counts, and per-field sum/avg/min/max aggregations, and refresh its cached results.


A report is a query surface as well as a saved configuration. Once a report has data sources, the Report type exposes five read fields — records (the combined record list), recordCount (the matching count), todos (the record list as Todo objects), todoCount (the matching count as Todo objects), and aggregations (per-field statistics) — plus the refreshReportAggregations mutation, which invalidates the cached aggregation results.

All five read fields run with the report creator’s workspace permissions (not the caller’s) and combine every data source on the report, so the data you read is identical for every viewer regardless of which workspaces they can see. See Query Reports for reading a report’s metadata and configuration, and Create a Report for setting up its data sources.

Request

Read the combined record list, count, and a sum/avg over a numeric custom field in one query:

query ReadReportData {
  report(id: "report_123") {
    todoCount
    todos(limit: 50, sort: [duedAt_ASC]) {
      id
      title
      done
      duedAt
      users {
        fullName
      }
    }
    aggregations(fields: [{ field: "field_123", fieldType: "number" }]) {
      field
      fieldName
      sum
      avg
      min
      max
      count
    }
  }
}

report is identified by the report’s id. records, recordCount, todos, todoCount, and aggregations are fields on the resolved Report, so they are always nested under a report (or reports) query.

Parameters

These fields share the records TodosFilter and TodosSort inputs rather than redefining them. See the Records section for the full field-by-field reference of those inputs — only the report-specific behavior is documented here.

Report.todos arguments

ParameterTypeRequiredDescription
filterTodosFilterNoExtra record filter applied on top of each data source’s stored filter. Its projectIds are ignored — workspace scope comes from the data sources, not from this filter.
sort[TodosSort!]NoSort order. Accepts the records TodosSort enum values (e.g. duedAt_ASC, title_DESC, createdAt_DESC).
limitIntNoMaximum records to return. Defaults to 100; capped at a hard maximum of 5000. Values above 5000 are clamped, not rejected.
skipIntNoNumber of records to skip before returning results, for paging through large reports.

Report.todoCount arguments

ParameterTypeRequiredDescription
filterTodosFilterNoSame extra filter as todos. Returns the count of matching records across all data sources.

Report.records arguments

ParameterTypeRequiredDescription
filterTodosFilterNoExtra record filter applied on top of each data source’s stored filter. Its projectIds are ignored — workspace scope comes from the data sources, not from this filter.
sort[TodosSort!]NoSort order. Only the first key applies; the default is createdAt_DESC. Keys the row projection cannot sort by (for example custom-field keys) fall back to the default order.
limitIntNoMaximum records to return. Defaults to 100; capped at a hard maximum of 5000. Values above 5000 are clamped, not rejected.
skipIntNoNumber of records to skip before returning results, for paging through large reports. The reader walks to skip + limit before slicing, so paging works past the 5,000-row cap.

ReportRecord

ReportRecord is Report.records’ flat projection of a record. It carries the same record data as Todo, plus the home table: databaseId and databaseName name the record’s table.

FieldTypeDescription
idID!Unique record identifier.
titleString!The table’s display field’s text; the record’s ID when it has none.
textString!The description field’s text. Empty string when none.
htmlStringThe description field’s HTML. null when none.
doneBoolean!Completion: the completion field is a checked checkbox, or a select value in the table’s “complete values”.
startedAtDateTimeStart of the table’s bound due-date value. null when unset.
duedAtDateTimeEnd of the table’s bound due-date value. null when unset.
timezoneStringThe due date’s timezone. null when unset.
dueDateGranularityStringThe due date’s granularity. null when unset.
createdAtDateTime!When the record was created.
updatedAtDateTime!When the record was last modified.
completedAtDateTimeWhen the record was completed. null when not completed.
workspaceIdID!The record’s workspace.
workspaceNameString!The record’s workspace name.
workspaceSlugString!The record’s workspace slug.
databaseIdIDThe record’s home table. null when there is none.
databaseNameStringThe home table’s name. null when there is none.
fields[RecordSnapshotField!]!The record’s field values on the home table.
assignees[User!]!The assignees from the table’s bound assignee field.
tags[ReportRecordTag!]!The tags from the table’s bound tags field.

Report.recordCount arguments

ParameterTypeRequiredDescription
filterTodosFilterNoSame extra filter as records. Returns the count of matching records across all data sources, one row per record — a record mounted in several selected tables counts once.

Report.aggregations arguments

ParameterTypeRequiredDescription
fields[AggregationFieldInput!]!YesThe fields to aggregate. One FieldAggregation is returned per entry.
filterTodosFilterNoExtra record filter, applied on top of each data source’s filter before aggregating.

AggregationFieldInput

FieldTypeRequiredDescription
fieldString!YesThe field to aggregate. A custom field’s id, a default field key (e.g. timeTracking.totalDuration), or a merged_… key for merged fields.
originalIds[String!]NoFor a merged/mapped field, the underlying custom field IDs it spans. Each record contributes at most one value across them.
fieldTypeStringNoThe field’s type hint, echoed back on the result as FieldAggregation.fieldType.
footerTypeStringNoDisplay hint passed through from the app’s report footer config. Does not change the computed statistics.

Response

{
  "data": {
    "report": {
      "todoCount": 128,
      "todos": [
        {
          "id": "clm4n8qwx000008l0g4oxdqn7",
          "title": "Renew enterprise contract",
          "done": false,
          "duedAt": "2026-06-02T00:00:00.000Z",
          "users": [{ "fullName": "Dana Reyes" }]
        },
        {
          "id": "clm4r2abc000108l0aaaa1111",
          "title": "Q3 onboarding kickoff",
          "done": true,
          "duedAt": "2026-06-05T00:00:00.000Z",
          "users": []
        }
      ],
      "aggregations": [
        {
          "field": "field_123",
          "fieldName": "Deal Value",
          "sum": 184500,
          "avg": 1441.4,
          "min": 0,
          "max": 50000,
          "count": 128
        }
      ]
    }
  }
}

FieldAggregation

FieldTypeDescription
fieldString!Echoes the field from the matching AggregationFieldInput, so you can correlate inputs to results.
fieldNameStringHuman-readable name derived from the field. null when it cannot be resolved.
fieldTypeStringThe fieldType hint passed in, echoed back. null when not supplied.
sumFloatSum of the numeric values. null when no numeric values matched.
avgFloatMean of the numeric values. null when no numeric values matched.
minFloatSmallest numeric value. null when no numeric values matched.
maxFloatLargest numeric value. null when no numeric values matched.
countInt!Number of records that contributed a numeric value. 0 when none matched.

todos returns [Todo!]! — select any field on Todo (see the Records section for the full type). records returns [ReportRecord!]! — a flat projection of each record; see the ReportRecord table above. recordCount and todoCount return a bare Int!. aggregations returns [FieldAggregation!]!, one entry per requested field, in the same order.

Empty reports

A report with no data sources is valid but has nothing to read:

  • records and todos return an empty list [].
  • recordCount and todoCount return 0.
  • aggregations returns an empty list [].

No error is raised in any of these cases. Add data sources with Create a Report or Manage Report Access before expecting results.

Refreshing cached aggregations

aggregations results are cached in Redis (in 5-minute buckets) to keep large reports fast. Use the refreshReportAggregations mutation to discard that cache when the underlying records have changed and you need the next read to recompute.

mutation RefreshReportAggregations {
  refreshReportAggregations(reportId: "report_123") {
    id
    lastGeneratedAt
  }
}

refreshReportAggregations arguments

ParameterTypeRequiredDescription
reportIdString!YesThe report whose aggregation cache to invalidate.
{
  "data": {
    "refreshReportAggregations": {
      "id": "clm4n8qwx000008l0g4oxdqn7",
      "lastGeneratedAt": "2026-05-29T12:00:00.000Z"
    }
  }
}

Errors

CodeWhen
REPORT_NOT_FOUNDThe report/reportId does not exist, or you are neither the creator nor a shared user of the report.
UNAUTHENTICATEDThe request has no valid credentials.
FORBIDDENThe request is authenticated but carries no organization context.

Reading data via report { records / recordCount / todos / todoCount / aggregations } is gated by the parent report query’s visibility check, which raises REPORT_NOT_FOUND when the report is invisible to you. refreshReportAggregations performs the same view-access check before touching the cache.

Permissions

  • Reading records / recordCount / todos / todoCount / aggregations — requires view access to the parent report: you must be its createdBy or appear in its reportUsers (either EDITOR or VIEWER). The records themselves are resolved with the report creator’s workspace permissions, so a VIEWER who cannot see a given workspace directly still sees its records through the report.
  • refreshReportAggregations — requires only view access (creator or any shared user, EDITOR or VIEWER). It is intentionally looser than modifying the report; invalidating the cache is a read-side operation.

Modifying or deleting a report requires higher tiers — see Manage Report Access and Duplicate & Delete a Report.