cube-js/cube · error
Granularity "${timeDimension.granularity}" not found in time
Error message
Granularity "${timeDimension.granularity}" not found in time dimension "${timeDimension.dimension}" What it means
ResultSet.timeSeries() builds the x-axis date buckets for a time-series result. When a time dimension uses a custom granularity defined in the data model viagranularities annotations, the resultset annotation must contain an entry named `dimension.granularity`. This error is thrown when the requested granularity is neither a built-in interval nor present in the annotations, so no series can be generated.
Source
Thrown at packages/cubejs-client-core/src/ResultSet.ts:488
if (!dateRange) {
return null;
}
const padToDay = timeDimension.dateRange ?
(timeDimension.dateRange as string[]).find(d => d.match(DateRegex)) :
!['hour', 'minute', 'second'].includes(timeDimension.granularity);
const [start, end] = dateRange;
const range = dayRange(start, end, annotations);
if (isPredefinedGranularity(timeDimension.granularity)) {
return TIME_SERIES[timeDimension.granularity](
padToDay ? range.snapTo('d') : range
);
}
if (!annotations?.[`${timeDimension.dimension}.${timeDimension.granularity}`]) {
throw new Error(`Granularity "${timeDimension.granularity}" not found in time dimension "${timeDimension.dimension}"`);
}
return timeSeriesFromCustomInterval(
start, end, annotations[`${timeDimension.dimension}.${timeDimension.granularity}`].granularity!
);
}
/**
* Base method for pivoting [ResultSet](#result-set) data.
* Most of the time shouldn't be used directly and [chartPivot](#result-set-chart-pivot)
* or [tablePivot](#table-pivot) should be used instead.
*
* You can find the examples of using the `pivotConfig` [here](#types-pivot-config)
* ```js
* // For query
* {
* measures: ['Stories.count'],
* timeDimensions: [{View on GitHub (pinned to 7d981676b3)
Solutions
- Verify the granularity name in the query timeDimensions exactly matches a granularity name declared in the cube's granularities (or a builtin: day/week/month/quarter/year/hour/minute/second)
- Inspect resultSet.loadResponses[0].annotation for a key `<dimension>.<granularity>`; if missing, ensure the backend returns full annotations (check proxy/caching code that may strip annotation)
- Update @cubejs-client/core to a version supporting custom granularities and confirm the backend supports them
- As a fallback, use built-in granularities or build the series manually from rawData()
Example fix
// before
granularity: 'Qtr' // typo, not defined in the cube
// after
// cube schema
dimension XYZ { timeDimension { granularities: [quarter, quarterOfDay] } }
// query
granularity: 'quarterOfDay' // must match the schema-declared name and appear in annotations Defensive patterns
Strategy: validation
Validate before calling
function canBuildTimeSeries(resultSet) {
const td = resultSet.loadResponse?.query?.timeDimensions?.[0];
if (!td || !td.granularity) return true; // builtin path
const key = `${td.dimension}.${td.granularity}`;
return Boolean(resultSet.loadResponse?.annotation?.[key]);
}
if (!canBuildTimeSeries(rs)) { console.warn('custom granularity missing from annotations', td); return; } Type guard
function hasGranularityAnnotation(rs: ResultSet): boolean {
const td = rs.loadResponse?.query?.timeDimensions?.[0];
if (!td?.granularity) return true;
return rs.loadResponse?.annotation?.[`${td.dimension}.${td.granularity}`] != null;
} Try / catch
let series;
try { series = rs.seriesNames(); } catch (e) {
if (String(e?.message).includes('not found in time dimension')) {
series = []; // render empty chart and surface schema/query mismatch
} else throw e;
} Prevention
- Keep granularity names in queries generated from the same source as the schema (codegen/constants)
- Assert annotation completeness in tests for any custom-granularity dashboards
- Never strip or partially copy loadResponse annotation in proxies/caches
- Prefer built-in granularities unless custom intervals are truly needed
When it happens
Trigger: Calling chart-generating methods (seriesNumericColumns/series → timeSeries) on a ResultSet whose query used a timeDimension with a custom (non-builtin) granularity while the loadResponse annotation lacks a `<dimension>.<granularity>` key — e.g. annotation omitted, annotation stripped by a proxy, or a typo/mismatch between the granularity name in the query and the granularity name declared in the data model's granularities list.
Common situations: Custom granularities declared in the cube schema (e.g. granularities: [{name:'quarter_hour'}]) but queried with a different casing/spelling; caching or mock loadResponses built without annotations; backend versions older than custom-granularity support returning annotations without granularity entries; passing a ResultSet produced by decompose/comparison handling where the wrong sub-result's annotations are consulted.
Related errors
- compareDateRange can only exist for one timeDimension
- QuestDB custom granularity has an unparseable origin: ${orig
- Expected one parameter but nothing found
- Expected only 2 parameters for timestamp filter but got: ${t
- member attribute is required for filter ${JSON.stringify(f)}
AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02).
Data as JSON: /api/errors/6dc03cd9a58f19ce.
Report an issue: GitHub.