Studio calls your engine's execute() method with one or more AgExecuteRequest objects. Each request carries an AgStudioQuery that your engine translates into your backend's native query language. Return one AgExecuteResult per request, in the same order.
Query Anatomy Copy Link
Every field reference in an AgStudioQuery is an AgStudioQueryField object carrying key, fieldId, sourceId, and optional properties like aggregation.
Core Fields Copy Link
| Field | Purpose | Shape |
|---|---|---|
axes | Group-by dimensions for aggregation | [{ dimensions: [{ field: { key, fieldId, sourceId } }] }] |
measures | Aggregated columns | [{ field: { key, fieldId, sourceId, aggregation: 'sum' } }] |
projection | Raw columns when not aggregating (mutually exclusive with axes/measures) | [{ field: { key, fieldId, sourceId } }] |
filter | WHERE clause tree (recursive groups with combinator, leaves with operator) | { combinator: 'and', conditions: [{ field, operator: 'equals', value }] } |
sort | ORDER BY specification | [{ field: { key, fieldId, sourceId }, direction: 'desc' }] |
limit | LIMIT and OFFSET | { count: 100, offset: 20 } |
computedFields | Computed columns with expression ASTs and evaluation phase | See Computed Fields |
Additional Fields Copy Link
These fields appear when the dashboard uses features that require them. If your engine does not support a given field, the corresponding UI feature will not work correctly.
| Field | Purpose |
|---|---|
having | Post-aggregation filter (SQL HAVING) |
window | Window functions (rank, denseRank, rowNumber) |
joins | Multi-source joins (see Sources and Joins) |
scope | Dimension-member slice for slicer/page-filter semantics |
distinct | Row deduplication for projection-mode queries |
from | Derived-table composition (SQL FROM (SELECT ...)) |
Properties available on the AgStudioQuery interface.
Dimension axes for OLAP aggregation. Only axes[0] is consumed in this engine revision — additional entries are reserved for future pivot/hierarchical modes. Empty axes: [] combined with measures yields a single grand-total group. Mutually exclusive with projection.
|
Measures aggregated at each cell. Required when axes is present. |
Output columns for detail/raw-row queries. Mutually exclusive with axes/measures.
|
Ordered join chain — the query's source topology. Omit for a single-source query. Engines must consume clauses in order and must not perform schema lookup to reconstruct the topology.
|
Row-level predicate applied BEFORE any aggregation (SQL WHERE semantics). Leaves reference source fields on the joined plan. Affects both the result set and the denominator used by measures with totalsScope: 'filtered'.
|
Post-aggregation predicate applied to cells, AFTER measures are computed (SQL HAVING semantics). Leaves must reference measure aliases (via AgExprRef) or dimension fields; row-level source fields are rejected. Cells the predicate rejects are dropped; the measure denominators are NOT recomputed.
|
Dimension-member slice (MDX-inspired). Restricts the visible cells to the chosen members but — unlike filter — does NOT remove the excluded rows from the denominator used by measures with totalsScope: 'unrestricted'. Use for slicer/page-filter semantics where "% of total" should still divide by the full dataset.
|
AgStudioSortDefinition[] |
AgStudioWindowDefinition[] |
AgStudioLimitDefinition |
Projection-mode row deduplication (SQL SELECT DISTINCT). In aggregation mode, use distinct-within-measure aggregations (countd etc.) instead.
|
Evaluation schedule for computed expression fields. Topologically sorted — engines evaluate in array order. Each entry carries the expression in AgStudioExpression AST form and an explicit evaluation phase. Synthetic entries decompose cross-source expressions into single-source intermediates. Absent when the query has no expression fields.
|
Derived-table composition (SQL FROM (SELECT ...)). Engines execute the inner query, materialise its result, and run the outer query over those rows. Each materialised level costs memory and latency, so engines should consider imposing a depth limit and rejecting deeper nesting with an error rather than silently truncating.
|
Computed Fields Copy Link
computedFields is a topologically sorted evaluation schedule. Each entry carries an expression AST and an explicit evaluation phase:
phase: 'pre-agg': evaluated on source rows before grouping (SQL column expression)phase: 'post-agg': evaluated on grouped output after measures are computedsynthetic: true: internally generated intermediates; exclude from user-facing result columns
Engines evaluate entries in array order. Each entry's dependencies are satisfied by prior entries or source scans.
Expression nodes are either operations ({ operator, inputs, options? }) or leaves: field references ({ field: AgStudioQueryField }), literals ({ literal: Primitive }), or alias references ({ ref: string }).
Field Identifiers Copy Link
Every field reference in a query is an AgStudioQueryField:
| Property | Where it comes from | What to use it for |
|---|---|---|
fieldId | Source-qualified column ID from getDataSources(): "sales.revenue". For computed fields (sourceId: ''), a bare identifier like "pct_of_total". | Map to a backend column. Strip the source prefix for source fields; use as-is for computed fields. |
sourceId | The id of the source the field belongs to: "sales". | Pick which backend table or endpoint the query targets. |
key | An opaque string Studio builds per-field per-query. | The column name in your result rows. Your result rows[i][field.key] must round-trip cleanly. Treat key as opaque; do not parse it. |
Measure fields also carry aggregation (e.g. 'sum', 'avg', 'count'). Access it via measure.field.aggregation.
Properties available on the AgStudioQueryField interface.
Composite identity key and output column alias. Treat as opaque — built by buildFieldKey. |
Owning source ID; '' for computed/inline fields. |
The underlying field identifier within the source. |
Aggregation function for measure fields; omitted for raw dimensions. |
Additional identity component for fields that would otherwise collide on (sourceId, fieldId, aggregation) — e.g. window outputs with different orderings.
|
Alias for self-joins — disambiguates multiple instances of the same underlying source in one query.
|
Inline expression AST — present for computed / inline fields (where sourceId === '') and also the mechanism for granularity transforms such as dateTrunc and numeric bucketing.
|
Inline-field intent disambiguator. For inline fields, true marks a measure (lives under AgStudioQuery.measures), false marks a dimension (lives under axes[].dimensions). Validator rejects mismatched slot/intent pairs.
|
Optional data-type hint. Resolver-produced fields set this from the schema; hand-constructed queries may omit it, in which case the hydration step fills it in. HydratedField widens this to required.
|
Filter Structure Copy Link
The filter and having fields share the same recursive tree structure:
AgStudioFilterGroup: a boolean combinator ('and', 'or', or 'not') wrapping child nodes:
{
combinator: 'and',
conditions: [/* AgStudioFilterCondition | AgStudioFilterGroup */],
}
AgStudioFilterCondition: a leaf predicate on a single field:
{
field: { key, fieldId, sourceId },
operator: 'equals',
value: 'EMEA',
}
Walk the tree recursively: groups become parenthesised boolean expressions; conditions become backend predicates. See the reference examples for complete filter translation.
Properties available on the AgStudioFilterCondition interface.
AgStudioQueryField |
'equals' | 'notEqual' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'isNull' | 'isNotNull' | 'between' | 'isIn' | 'isTrue' | 'isFalse' |
undefined for isNull / isNotNull / isTrue / isFalse; a 2-element array for between (check operator to distinguish from isIn); an array for isIn; single primitive otherwise.
|
Routing metadata carried to the engine and explain output. |
Properties available on the AgStudioFilterGroup interface.
'and' | 'or' | 'not' |
AgStudioFilterDefinition[] |
Result Format Copy Link
Return an AgExecuteResult per request, discriminated on dataShape. Return 'rows' (default) or 'columns' depending on options.shape.
The dataShape discriminator on each result must match the data you return. If you set dataShape: 'columns' but populate rows (or vice versa), widgets will render empty.
Both shapes carry an AgResultMetadata object. rowCount is the number of rows in the returned result. When your engine applies a limit, set totalRowCount to the pre-limit row count.
Rows Format (default) Copy Link
return {
dataShape: 'rows',
rows: [
{ 'sales.region': 'EMEA', 'sales.revenue': 123456 },
{ 'sales.region': 'APAC', 'sales.revenue': 234567 },
],
metadata: { rowCount: 2, totalRowCount: 5432 },
};
Columns Format Copy Link
const columns = new Map<string, ReadonlyArray<Primitive | null>>();
columns.set('sales.region', ['EMEA', 'APAC']);
columns.set('sales.revenue', [123456, 234567]);
return {
dataShape: 'columns',
columns,
metadata: { rowCount: 2 },
};
Properties available on the AgExecuteRequest<TShape extends AgResultShape = AgResultShape> interface.
The query to execute against the data source. |
Per-request options controlling result shape, cancellation, and validation. |
Advisory metadata for logging, tracing, and batch coalescing. |
Properties available on the AgRowsResult interface.
Discriminator — always 'rows' for this shape. |
Result rows keyed by output field alias. |
Row count and optional pre-limit total. |
Properties available on the AgColumnsResult interface.
Discriminator — always 'columns' for this shape. |
Column arrays keyed by output field alias. |
Row count and optional pre-limit total. |
Sources and Joins Copy Link
When a widget pulls fields from multiple related sources (declared via Relationships), Studio produces a single AgStudioQuery with joins populated. Your execute() receives one query that spans multiple sources. Each field's sourceId and fieldId identify which backend table it belongs to, so your translator can resolve every reference.
If your backend does not support joins, throw a descriptive error when a query contains joins.
Batching and Cancellation Copy Link
Studio calls execute(...requests) with every request in the current render cycle. Each request carries an info object:
batchId: All requests in the sameexecute()call share abatchId. Use it to coalesce backend round-trips (e.g. one combined request per batch). When absent, treat each request independently.queryId: Distinguishes independent queries from the same widget (e.g.'rows'vs'grandTotal'). Studio useswidgetId + queryIdas the cancellation key.
Each request carries an optional options.signal: AbortSignal. Propagate it into your backend call to cancel superseded queries:
const res = await fetch(url, { signal: request.options?.signal });
Errors Copy Link
Throw from execute() when the backend fails. Any failed request in a batch will cause the whole batch to fail. This is to avoid partial data flowing into Studio.
async execute(...requests: AgExecuteRequest<AgResultShape>[]): Promise<AgExecuteResult[]> {
try {
return await this.runQueries(requests);
} catch (error) {
throw { message: 'Backend query failed', cause: error };
}
}
Reference Examples Copy Link
These are reference implementations for learning purposes. They are not production-ready integrations and will not cover every query feature. Use them as starting points for your own engine.
ClickHouse over HTTP Copy Link
Translates AgStudioQuery to ClickHouse SQL, posts it over HTTP, and returns the response. The dashboard queries ClickHouse's uk_price_paid dataset (~28M rows) without downloading any of it.
Contains HM Land Registry data © Crown copyright and database right 2021. This data is licensed under the Open Government Licence v3.0. (Source, Fields)
REST API: World Bank Countries Copy Link
Queries the World Bank Open Data API. Supported predicates are sent as URL parameters; remaining filters, aggregation, and sorting are applied locally.
Contains data from The World Bank: Countries API, licensed under Creative Commons Attribution 4.0 (CC BY 4.0). (Terms of Use)