Dataset AI tools — the inputs and outputs of list_datasets and query_dataset.
Dataset AI tools — the inputs and outputs of list_datasets and
query_dataset (ADR-0149 D2).
The two tools give an AI the business semantic layer a person already reads
through dashboards and reports: list_datasets returns the catalogue (the
datasets the caller may query, their dimensions and measures, each member's
business vocabulary, and which of the caller's words matched which member),
and query_dataset runs a DatasetSelection against one saved dataset and
answers rows plus a provenance block naming the dataset and the measures
used. The shapes are declared here once and projected to the JSON Schema an
AIToolDefinition carries (getDatasetToolJsonSchemas), so the MCP
registration and the product AI's registration read one definition.
query_dataset's selection is DatasetSelectionSchema BY REFERENCE, the
body POST /api/v1/analytics/dataset/query takes under the same key: the AI
has no private dialect, and the selection's own aliases and guidance apply
inside it. Only a saved dataset is addressed, by datasetName: there is no
inline dataset and no previewDrafts on this tool, because an AI that could
author a dataset inline could author its own 口径.
Only the shapes are spec. The executor, the catalogue builder with its
admission and member withholding, the term matcher and the row cap
(selection.limit held to the bridge's maxQueryLimit) belong to
@objectstack/service-analytics (card #31589); the MCP registration under
data:read to @objectstack/mcp (card #31590). The tool descriptions, which
carry the ADR-0149 D3 selection rule to the model, are written by the
executor: AI guidance text does not enter the spec (ADR-0149 M1, ruled A).
Dataset name — what query_dataset takes as datasetName
label
string
✅
Dataset label at the caller's locale
description
string
optional
Dataset description at the caller's locale; absent when the dataset declares none
object
string
✅
The dataset's base object, by machine name
membersWithheld
true
optional
Present, and true, when a member was left out because this caller cannot read a field it reads: the narrowing is stated, so an answer can say a measure exists that this caller may not see (ADR-0090 D10). Absent when every member the dataset publishes is listed
Each of the caller's terms that matched a member of this dataset, computed by the platform under the same normalization the dataset's own validation uses. Absent when no terms were sent
Candidate business words lifted from the question (e.g. ["营收", "销售阶段"]). Each is compared, after normalization (normalizeDatasetTerm), with every listed member's name, label and synonyms, and each hit is reported on its dataset's matches. Omit it to list the whole catalogue.
Dataset name — what query_dataset takes as datasetName
label
string
✅
Dataset label at the caller's locale
description
string
optional
Dataset description at the caller's locale; absent when the dataset declares none
object
string
✅
The dataset's base object, by machine name
membersWithheld
true
optional
Present, and true, when a member was left out because this caller cannot read a field it reads: the narrowing is stated, so an answer can say a measure exists that this caller may not see (ADR-0090 D10). Absent when every member the dataset publishes is listed
Each of the caller's terms that matched a member of this dataset, computed by the platform under the same normalization the dataset's own validation uses. Absent when no terms were sent
The saved dataset to query, by name, as list_datasets lists it. A dataset that does not exist and one this caller may not query get the same NOT_FOUND answer
Time-bucketed dimensions. Each entry names a dimension, an optional bucket granularity, and an optional dateRange — a preset name from the closed date-range vocabulary (e.g. 'last_7_days') or an explicit [start, end] window; an unrecognised string answers 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED instead of silently widening.
Presentation-scope date bucketing applied to every selected date dimension; an explicit timeDimensions entry wins over it, and the dataset dimension's own default is used when neither is set
order
Record<string, Enum<'asc' | 'desc'>>
optional
limit
integer
optional
Maximum number of rows to return, applied after order — a non-negative integer (0 returns no rows)
offset
integer
optional
Number of rows to skip before the first row returned, applied after order — a non-negative integer; an offset with no limit returns every row after it
Marginal aggregates - one entry per requested totals grouping, in request order, each computed with the measure's true aggregate over the underlying data (never re-derived from bucketed values). The grand-total grouping yields a single dimensionless row.
object
string
optional
The base object of the dataset the answer was computed from: the dataset's object, by machine name. Every dataset answer (POST /analytics/dataset/query) must carry it, whatever dimensions are selected and whether or not rows came back, so a consumer can refresh on that object's record changes and drill into its records. Absent on a cube query answer, which has no dataset behind it.
dimensionFields
Record<string, string>
optional
Drill-through sidecar: each equality-drillable dimension the answer is grouped by, dimension name to that dataset dimension's own field (a base-object field or a relationship.field path). Set only on a dataset answer (POST /analytics/dataset/query) that has rows and groups by at least one dimension with a field that is not type: 'date'; a date dimension is never listed (see drillRanges). Absent on a cube query answer.
drillRawRows
Record<string, any>[]
optional
Drill-through sidecar, aligned to rows by index: each dimensionFields dimension name to the stored value that row was grouped by (a select option's value, a lookup's record id), captured before label resolution rewrites the row to its display label. An exact-match drill filter is built from these, not from the labels in rows. Set exactly when dimensionFields is.
drillRawTotals
Record<string, any>[][]
optional
Drill-through sidecar for totals: entry [i][j] aligns to totals[i].rows[j] and holds the stored value of the dimensionFields dimensions that grouping groups by, so the grand-total grouping yields an empty map per row. Set only when dimensionFields is and the answer carries at least one totals grouping.
Drill-through sidecar for date ranges, aligned to rows by index: date dimension name to the half-open range [gte, lt) that row's records fall in on that dimension's field. A time bucket gets the span it covers; a timeDimensions[].dateRange window gets the window the query filtered by on every row, or narrows the bucket entry on the same field to the part inside the window. Bounds are YYYY-MM-DD calendar days, or ISO-8601 instants when the source field is datetime (a bucket's at midnight in the reference timezone); a row whose bucket value is null gets no bucket entry, and a window whose end is an instant or the last supported day gets none. Set only on a dataset answer that has rows and groups by at least one type: 'date' dimension with a field and a granularity, or carries a dateRange on a dimension with a field; independent of dimensionFields. Absent on a cube query answer.
drillScopes
Record<string, any>
optional
Drill-through sidecar per measure: each selected measure name to the list filter, on the answer's object, that selects the records that measure aggregates: the dataset's own filter, AND the measure's own filter, AND, for a measure over a field, that field holding a value ({ "$null": false }; both instants for a duration measure), with each {token} resolved as the query resolved it; {} when none of these applies. A drill lists the clicked bucket (dimensionFields / drillRawRows / drillRawTotals, drillRanges) AND the widget's own filter AND this scope. A measure is left out when no list filter on object selects its records (a derived measure; a scope that names a relationship path such as account.region, as a key or as a $field reference), and a measure left out offers no drill. Keyed by measure name: a __compare column has no entry. Set on every dataset answer (POST /analytics/dataset/query) that has rows; absent on a cube query answer and on a draft-preview answer.
The dataset and members the answer was computed from
rowCount
integer
✅
How many rows rows holds
truncated
boolean
✅
true when more rows existed past the row cap the tool applied and were not returned
delegationNarrowed
true
optional
Present, and true, when the caller acts as an agent whose authority narrows what it may read on the dataset's object; warning states the narrowing (ADR-0090 D10)
warning
string
optional
The narrowing statement; present exactly when delegationNarrowed is
Column data type, in the DimensionType vocabulary (string / number / boolean / time / geo). A dimension column carries its cube dimension's type; a measure column is number except for min/max over a date/datetime/time field, which is time — those aggregates return a value of the aggregated field's own type. count/count_distinct, sum/avg and derived measures stay numeric.
label
string
optional
Human display label (e.g. measure label) — for legends/KPIs.
format
string
optional
Display format hint (e.g. measure format like "$0,0", "0.0%").
currency
string
optional
Resolved ISO 4217 code for a MONETARY measure (explicit measure currency, then the source field's fixed currency — its currencyConfig.defaultCurrency, read only under currencyMode: 'fixed' — then the tenant default). Absent on non-monetary columns, which must never render a symbol, and on a column that carries currencyField.
currencyField
string
optional
[ADR-0144 D2] The PER-ROW currency of a measure column over a currencyMode: 'record' amount: the name of the dimension column in this answer that groups by the amount's code field, whose value in each row is that row's ISO 4217 code. currency is absent when this is present. Only on a queryDataset measure column that takes the sum, avg, min or max of such an amount; never on a count.
percentScale
Enum<'fraction' | 'whole'>
optional
The column's percent SCALE, when it is a percentage: fraction for a 0-1 ratio (1 renders as "100%"), whole for percentage points (1 renders as "1%"). Resolved from metadata; absent when the column is not a percentage. Renderers that receive it must scale by it instead of guessing from the value.
Closed aggregate discriminator for a measure column whose display name is the server's built-in default: the dataset measure declared an aggregate and no label. A renderer may substitute its own localized name for the aggregate. Absent whenever the author declared a label, and on dimension / derived columns.
The aggregate a measure column carries, in the closed AggregationFunction vocabulary: the dataset measure's own aggregate, stated whether or not the author declared a label, so a renderer can tell a count from a sum (integer axis ticks for a count, for example). Set on every measure column of a dataset answer (POST /analytics/dataset/query, the live query and the draft-data preview alike) whose measure declares an aggregate, its __compare column included. Absent on dimension columns and on derived measures, which have no single aggregate, and on a cube query answer (POST /analytics/query). Unlike builtinAggregate, it says nothing about who named the column.
nullLabel
string
optional
Display name of a dimension column's null bucket: the dataset dimension's nullLabel, resolved at the request locale like label. A renderer draws the column's null values under it instead of its own default. Set only on a dimension column of a dataset answer (POST /analytics/dataset/query) whose dimension declares one; absent on measure columns and on a cube query answer. Display copy only: grouping is unchanged.
polarity
Enum<'higher' | 'lower' | 'neutral'>
optional
Which direction of change is good news for a measure column: the dataset measure's polarity (higher / lower / neutral), relayed as declared. A renderer colours a period-over-period delta by it: lower draws a rise as bad, neutral draws no good or bad colour. Set on a measure column of a dataset answer (POST /analytics/dataset/query, the live query and the draft-data preview alike) whose measure declares one, its __compare column included; absent otherwise, on dimension columns, and on a cube query answer. Absent means a rise is drawn as good.