AG Studio Launch Week 🚀🚀🚀 28 Sep - 2 Oct 2026 🚀🚀🚀 Join now

React Embedded AnalyticsCustom Engine

Version 3.0.0

A custom engine replaces the built-in engine entirely, translating each query into the backend's own query language.

You may not need a custom engine. The built-in engine accepts arrays of rows via Loading Data. Only replace it when you need to push query execution to a backend - see When You Don't Need One below.

Dashboards can place severe load on data backends. Ensure the backends you communicate with are scaled suitably for your use case.

The example below uses a custom engine to demonstrate how to set up server-side data access.

When to Use One Copy Link

The built-in engine loads all row data into the browser and runs operations in-memory. This works well when the dataset fits in browser memory, you can afford the initial transfer latency, and you want instant filtering and sorting with no server round-trip.

Replace it with a custom engine when:

  • Dataset is too large: The data cannot be shipped to the browser and must be queried remotely.
  • Analytics backend: You already have a system (e.g. ClickHouse, Snowflake, BigQuery, a REST API) that serves aggregated data.
  • Computation delegation: You want to push aggregation, filtering, and sorting to a database engine rather than compute them client-side.

When You Don't Need One Copy Link

Stay on the built-in engine when:

  • Dataset fits in browser memory: Sync Data loads it once and every operation after that runs instantly, with no server round-trip.
  • Data is remote but manageable: Async Data fetches on demand from a database, warehouse, or API, while Studio still runs the query itself.
  • You want to avoid maintaining query translation: A custom engine takes on translating every AgStudioQuery shape Studio can produce, including joins, cubes, and computed fields - ongoing work each time Studio adds query capabilities.

The AgDataEngine Interface Copy Link

Implement the AgDataEngine interface. Your engine declares its data sources via getDataSources(), executes queries via execute(), and optionally performs async setup in init(). If your engine discovers its schema from a remote service, await that discovery in init() and return the result from getDataSources(). See Implementing Queries for how to translate a query once the interface is in place.

export class MyDataEngine implements AgDataEngine {
    async init(): Promise<void> {
        // Optional: async setup before the schema is consulted.
    }

    getDataSources(): AgDataSourcesDefinition {
        return {
            sources: [
                {
                    id: 'sales',
                    fields: [
                        { id: 'region', format: 'textFormat' },
                        { id: 'revenue', format: 'numberFormat' },
                    ],
                },
            ],
        };
    }

    async execute(...requests: AgExecuteRequest<AgResultShape>[]): Promise<AgExecuteResult[]> {
        return Promise.all(requests.map((req) => this.runOne(req)));
    }

    private async runOne(request: AgExecuteRequest<AgResultShape>): Promise<AgExecuteResult> {
        const { query } = request;
        const rows = await this.queryBackend(query); // Your backend call
        return { dataShape: 'rows', rows, metadata: { rowCount: rows.length } };
    }
}

Properties available on the AgDataEngine interface.

Function
Optional async lifecycle hook called by Studio before the engine is queried. Use this to bootstrap resources that must resolve before the schema is consulted - wasm compilation, HTTP fetches, database connections. Studio awaits this before calling getDataSources or finalize. Engines with no async setup can omit this entirely.
getDataSourcesCopy Link
Function
Declare the data sources the engine exposes to Studio. Called once, after init resolves. Studio uses this to build the canonical schema it operates against. The return value is the same AgDataSourcesDefinition shape a caller would pass on the data property when using the built-in engine. Engines that know their fields upfront can build this eagerly in a constructor; engines that discover fields from a remote service will typically await that discovery in init and return the resulting definition here. Schema is read once per engine lifecycle; if your underlying schema can change, rebuild the engine on the host application side.
finalizeCopy Link
Function
Called after getDataSources once Studio has built its schema view. Use this to perform any one-time post-schema setup. Engines with nothing to do here can omit this method.
executeCopy Link
Function
Execute one or more queries. Results are returned in request order, one AgExecuteResult per AgExecuteRequest. Studio batches requests that arrive together (typically within one render cycle). All requests in a batch share info.batchId. Engines that can coalesce backend calls should group by batchId. Cancellation: each request carries an optional options.signal that Studio aborts when the batch is superseded. Propagate it into your backend call (e.g. pass to fetch). Requests in the same call come from unrelated callers: one request's own cancellation or failure must not affect the result returned for any other request in the same call. Resolve each request's own AgExecuteResult independently - do not let the returned promise reject for a problem isolated to one request.
executeCubeCopy Link
Function
Execute one or more cube queries, returning one AgCubeResult per request. Optional capability - engines that implement this unlock pivot and nested-tree widgets over server-side data; engines that omit it cannot serve those widget types. As with execute, requests in the same call come from unrelated callers: resolve each request's own result independently, and don't let one request's cancellation or failure reject the returned promise for the whole call.
reloadCopy Link
Function
Invalidate any cached state so the next query recomputes against the current data. Engines without caches can omit this.
addEventListenerCopy Link
Function
Subscribe a listener to validation events the engine emits. Engines that never emit validation events can omit this. If you implement this method, you MUST also implement removeEventListener - Studio calls it on teardown.
removeEventListenerCopy Link
Function
Unsubscribe a listener previously registered via addEventListener.
disposeCopy Link
Function
Release large data structures to reduce GC pressure on page unload.
updateCopy Link
Function
Apply in-place updates to the engine's data sources. Engines that manage their data externally (read-only backends, on-demand fetchers) can omit this.

Using Your Engine Copy Link

const data = useMemo(() => { 
	return new MyDataEngine();
}, []);

<AgStudio data={data} />

Studio calls init() during startup, then getDataSources() once to freeze the schema. From that point on, Studio calls execute() as the user interacts with the dashboard.

getDataSources() is called once per engine lifecycle. The schema is frozen after that call; Studio will not re-read it. If your underlying schema changes at runtime (e.g. new columns added to a database), destroy the Studio instance and create a new one with a fresh engine.

Migrating from the Built-In Engine Copy Link

Swap the data property from an inline data definition to an engine instance:

Before:

const data = useMemo(() => { 
	return {
        sources: [{
            id: 'sales',
            fields: [/* ... */],
            data: [/* rows */]
        }]
    };
}, []);

<AgStudio data={data} />

After:

const data = useMemo(() => { 
	return new MyDataEngine();
}, []);

<AgStudio data={data} />

Extract the schema from your current config into your engine's getDataSources(), then implement query translation in execute(). See Implementing Queries for the full query anatomy.