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 three read fields — todos (the combined record list), todoCount (the matching count), and aggregations (per-field statistics) — plus the refreshReportAggregations mutation, which invalidates the cached aggregation results. Records are Todo objects in the API.

All three 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. 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.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.
Aggregations are numeric

sum, avg, min, and max are computed over numeric values only — number custom fields and numeric default fields such as time tracking. Non-numeric values are skipped, so a field with no numeric data returns count: 0 and null for every statistic.

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). todoCount returns 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:

  • todos returns an empty list [].
  • todoCount returns 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
  }
}
Refresh is not a synchronous recompute

refreshReportAggregations does not recompute aggregations itself. It invalidates the report’s cached aggregation results and sets lastGeneratedAt to the current time, then returns the updated Report. The numbers are recomputed lazily on the next aggregations query against the report. The returned Report does not contain fresh aggregation values — query aggregations afterwards to read them.

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 { 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 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.