---
product: "AG Studio"
title: "Loading Data"
description: "Load data into Studio's built-in engine from a sync data source already in memory, or an async data source fetched on demand."
framework: javascript
version: "3.0.0"
related:
    - title: "Data Overview"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/data/"
    - title: "Data Modelling"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/data-modelling/"
    - title: "Sharing & Caching Data"
      url: "https://www.ag-grid.com/studio/archive/3.0.0/javascript/sharing-caching-data/"
llms: "https://www.ag-grid.com/studio/archive/3.0.0/llms.txt"
---

# Loading Data

Data is loaded into the built-in engine one of two ways: synchronously, from data already held in memory, or asynchronously, fetched on demand from a database, warehouse, or API.

## Sync Data

Sync data sources can be used when data has already been loaded in the application.

A sync data source represents a single table of data.

#### Sync Data Source

```ts
import {
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
  enableStudioDevValidations,
} from "ag-studio";

if (process.env.NODE_ENV !== "production") {
  // Enable extended validations only for development
  enableStudioDevValidations();
}

const initialState: AgReportState = {
  pages: [
    {
      id: "page1",
      widgets: {
        "1": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "medals.country" },
              { id: "medals.sport" },
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
              { id: "medals.total", aggregation: "sum" },
            ],
          },
        },
        "2": {
          type: "column-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "medals.country" }],
            valueKey: [
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
            ],
            tooltipKey: [],
          },
        },
      },
      widgetLayout: {
        "1": {
          xTrack: 0,
          yTrack: 0,
          xSpan: 24,
          ySpan: 16,
        },
        "2": {
          xTrack: 0,
          yTrack: 16,
          xSpan: 24,
          ySpan: 16,
        },
      },
    },
  ],
  selectedPageId: "page1",
  panels: {
    filters: {
      collapsed: true,
    },
    edit: {
      collapsed: true,
    },
  },
};

const studioProperties: AgStudioProperties = {
  mode: "edit",
  initialState,
};

let studioApi: AgStudioApi;

// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);

fetch("https://www.ag-grid.com/studio/archive/3.0.0/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then((data) =>
    studioApi!.setProperty("data", { sources: [{ id: "medals", data }] }),
  );
```

[Live example: Sync Data Source](https://www.ag-grid.com/studio/archive/3.0.0/examples/loading-data/sync-data-source/typescript/)

```js
const studioProperties = {
    data: {
        sources: [{
            id: 'medals',
            data: [
                {
                    year: 2000,
                    sport: 'Swimming',
                    country: 'United States',
                    // ... other fields
                },
                // ... other rows
            ],
        }],
    },

    // other studio properties ...
}
```

Sync data sources are represented by the `AgSimpleDataSourceDefinition` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | Table ID |
| `name` | `string` |  | Table display name. If not provided, a formatted version of `id` will be used. |
| `description` | `string` |  | AI-facing description of this table's contents and purpose. |
| `data` | `TData[]` |  | Row data. |
| `fields` | `AgFieldDefinition<TRegistry, any, AgFormat<TRegistry>, any, any>[]` |  | Fields in the table. If not provided, will be inferred from the data. |

### Fields

By default, if no fields are provided, they will be inferred from the data.

It is also possible to provide and customise fields as part of the source definition.

#### Customising Fields

```ts
import {
  AgFieldDefinition,
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
  enableStudioDevValidations,
} from "ag-studio";

if (process.env.NODE_ENV !== "production") {
  // Enable extended validations only for development
  enableStudioDevValidations();
}

const initialState: AgReportState = {
  pages: [
    {
      id: "page1",
      widgets: {
        "1": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "medals.country" },
              { id: "medals.sport" },
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
              { id: "medals.total", aggregation: "sum" },
            ],
          },
        },
      },
      widgetLayout: {
        "1": {
          xTrack: 0,
          yTrack: 0,
          xSpan: 24,
          ySpan: 16,
        },
      },
    },
  ],
  selectedPageId: "page1",
  panels: {
    filters: {
      collapsed: true,
    },
    edit: {
      collapsed: true,
    },
  },
};

const fields: AgFieldDefinition[] = [
  {
    id: "athlete",
    format: "textFormat",
  },
  {
    id: "age",
    hide: true,
    format: "integerFormat",
  },
  {
    id: "country",
    name: "Location",
    format: "textFormat",
  },
  {
    id: "year",
    format: "integerFormat",
    formatOptions: { format: "0" },
  },
  {
    id: "date",
    format: "dateFormat",
  },
  {
    id: "sport",
    format: "textFormat",
  },
  {
    id: "gold",
    format: "integerFormat",
  },
  {
    id: "silver",
    format: "integerFormat",
  },
  {
    id: "bronze",
    format: "integerFormat",
  },
  {
    id: "total",
    format: "integerFormat",
  },
];

const studioProperties: AgStudioProperties = {
  mode: "edit",
  initialState,
};

let studioApi: AgStudioApi;

// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);

fetch("https://www.ag-grid.com/studio/archive/3.0.0/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then((data) =>
    studioApi!.setProperty("data", {
      sources: [{ id: "medals", data, fields }],
    }),
  );
```

[Live example: Customising Fields](https://www.ag-grid.com/studio/archive/3.0.0/examples/loading-data/sync-custom-fields/typescript/)

The example above demonstrates customising fields. The country field has been titled `Location`, and the age field has been hidden from the UI.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | Field ID. |
| `name` | `string` |  | Display name. |
| `description` | `string` |  | Field description. Displayed in the Field Panel |
| `hide` | `boolean` |  | Set to `true` to hide from being selected in the UI. Field can still be used for joins. |
| `editable` | `boolean \| AgFieldEditableKey[]` |  | Controls whether the field can be edited in the UI. |
| `serializer` | `AgFieldSerializer<InferDataTypeFromFormat<TRegistry, TFormat>>` |  | Optional. How the field values will be serialized into state. Defaults to format serializer. |
| `deserializer` | `AgFieldDeserializer<InferDataTypeFromFormat<TRegistry, TFormat>>` |  | Optional. How the field values will be deserialized from state. Defaults to format deserializer. |
| `createValueFormatter` | `AgFieldValueFormatterFactory<InferDataTypeFromFormat<TRegistry, TFormat>, TFormatOptions, any>` |  | Optional. Build a value formatter bound to the field's format options and the runtime API. Defaults to format factory. |
| `blankValue` | `string` |  | Optional. How blank values will be displayed. Defaults to format blank value. |
| `formatOptions` | `TFormatOptions` |  | Optional. Will be passed to the value formatter. |
| `context` | `TFieldContext` |  | Optional. An application-defined object carried on the hydrated field, and passed back on that field to callbacks such as a grid widget's `createCellRenderer`. Studio never reads it. Replaces any `context` set on the field's format. |
| `format` | `TFormat` |  | The format type of the field (provides default formatting, etc.). |
| `accessor` | `AgFieldDataAccessor<TData, InferDataTypeFromFormat<TRegistry, TFormat>>` |  | Optional. How to retrieve the value from the data. Either the property key, or a callback. If undefined, `id` will be used as the property key. |
| `cardinality` | `AgFieldCardinality` |  | Optional. Cardinality of the field data. Improves performance if provided. |
| `notBlank` | `boolean` |  | Optional. Does the field contain blank values. Improves performance if provided. |
| `supportedBuckets` | `string[]` |  | Optional. The buckets that this field supports. If undefined, will default to the `supportedBuckets` on the format. |

### Reloading Sync Data

Sync data can be reloaded by passing updated data sources to the `data` property.

Note that only the data will be updated. Data sources cannot be added or removed, and fields cannot be updated.

#### Reloading Sync Data

```ts
import {
  AgDataSourcesDefinition,
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
  enableStudioDevValidations,
} from "ag-studio";

if (process.env.NODE_ENV !== "production") {
  // Enable extended validations only for development
  enableStudioDevValidations();
}

const initialState: AgReportState = {
  pages: [
    {
      id: "page1",
      widgets: {
        "1": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "medals.country" },
              { id: "medals.sport" },
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
            ],
          },
        },
        "2": {
          type: "column-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "medals.country" }],
            valueKey: [
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
            ],
            tooltipKey: [],
          },
        },
      },
      widgetLayout: {
        "1": {
          xTrack: 0,
          yTrack: 0,
          xSpan: 24,
          ySpan: 16,
        },
        "2": {
          xTrack: 0,
          yTrack: 16,
          xSpan: 24,
          ySpan: 16,
        },
      },
    },
  ],
  selectedPageId: "page1",
  panels: {
    filters: {
      collapsed: true,
    },
    edit: {
      collapsed: true,
    },
  },
};

const studioProperties: AgStudioProperties = {
  mode: "edit",
  initialState,
};

let studioApi: AgStudioApi;

function generateData(
  sourceData: Record<string, any>[],
): Record<string, any>[] {
  return sourceData
    .slice(
      Math.floor(window.agRandom() * 100),
      200 + Math.floor(window.agRandom() * 100),
    )
    .map((row: any) => ({
      ...row,
      gold: Math.floor(window.agRandom() * 3),
      silver: Math.floor(window.agRandom() * 4),
      bronze: Math.floor(window.agRandom() * 4),
    }));
}

function reload() {
  fetch("https://www.ag-grid.com/studio/archive/3.0.0/example-assets/olympic-winners.json")
    .then((response) => response.json())
    .then((data) =>
      studioApi!.setProperty("data", {
        sources: [
          {
            id: "medals",
            data: generateData(data),
          },
        ],
      }),
    );
}

// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);

fetch("https://www.ag-grid.com/studio/archive/3.0.0/example-assets/olympic-winners.json")
  .then((response) => response.json())
  .then((data) =>
    studioApi!.setProperty("data", {
      sources: [
        {
          id: "medals",
          data: generateData(data),
        },
      ],
    }),
  );

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).reload = reload;
}
```

[Live example: Reloading Sync Data](https://www.ag-grid.com/studio/archive/3.0.0/examples/loading-data/sync-data-reload/typescript/)

## Async Data

Async data sources lazy load data on demand. This is the usual choice when your data lives in a database, a data warehouse, or behind an API, since Studio defers loading until a widget needs the data. Reducing how many rows cross the wire additionally requires the source to declare filter, sort or pagination support, described in [Data Source Filtering, Sorting, and Pagination](https://www.ag-grid.com/studio/archive/3.0.0/javascript/loading-data/#data-source-filtering-sorting-and-pagination) below.

An async data source represents one or more tables of data.

Async data sources still run entirely in the built-in engine: Studio fetches rows through `getData` and processes them in the browser. If your dataset is too large to fetch and process this way, or you want your own backend to execute queries directly, see [Custom Engine](https://www.ag-grid.com/studio/archive/3.0.0/javascript/server-side-data/) instead.

#### Async Data Source

```ts
import {
  AgFieldDefinition,
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
  enableStudioDevValidations,
} from "ag-studio";

if (process.env.NODE_ENV !== "production") {
  // Enable extended validations only for development
  enableStudioDevValidations();
}

const initialState: AgReportState = {
  pages: [
    {
      id: "page1",
      widgets: {
        "1": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "medals.country" },
              { id: "medals.sport" },
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
              { id: "medals.total", aggregation: "sum" },
            ],
          },
        },
        "2": {
          type: "column-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "medals.country" }],
            valueKey: [
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
            ],
            tooltipKey: [],
          },
        },
      },
      widgetLayout: {
        "1": {
          xTrack: 0,
          yTrack: 0,
          xSpan: 24,
          ySpan: 16,
        },
        "2": {
          xTrack: 0,
          yTrack: 16,
          xSpan: 24,
          ySpan: 16,
        },
      },
    },
  ],
  selectedPageId: "page1",
  panels: {
    filters: {
      collapsed: true,
    },
    edit: {
      collapsed: true,
    },
  },
};

const fields: AgFieldDefinition[] = [
  {
    id: "athlete",
    format: "textFormat",
  },
  {
    id: "age",
    format: "integerFormat",
  },
  {
    id: "country",
    format: "textFormat",
  },
  {
    id: "year",
    format: "integerFormat",
    formatOptions: { format: "0" },
  },
  {
    id: "date",
    format: "dateFormat",
  },
  {
    id: "sport",
    format: "textFormat",
  },
  {
    id: "gold",
    format: "integerFormat",
  },
  {
    id: "silver",
    format: "integerFormat",
  },
  {
    id: "bronze",
    format: "integerFormat",
  },
  {
    id: "total",
    format: "integerFormat",
  },
];

const studioProperties: AgStudioProperties = {
  mode: "edit",
  initialState,
  data: {
    sources: [
      {
        id: "medalsSource",
        dataShape: "row",
        getData: async () => {
          const response = await fetch(
            "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/olympic-winners.json",
          );
          const data = await response.json();
          return { data };
        },
        tables: [
          {
            id: "medals",
            fields,
          },
        ],
      },
    ],
  },
};

let studioApi: AgStudioApi;

// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
```

[Live example: Async Data Source](https://www.ag-grid.com/studio/archive/3.0.0/examples/loading-data/async-data-source/typescript/)

```js
const studioProperties = {
    data: {
        sources: [{
            id: 'medalsSource',
            dataShape: 'row',
            getData: async (tableId) => {
                const data = await fetchData(tableId);
                return { data };
            },
            tables: [
                {
                    id: 'medals',
                    fields: [
                        {
                            id: 'athlete',
                            format: 'textFormat',
                        },
                        // ... other fields
                    ],
                },
                // ... other tables
            ],
        }],
    },

    // other studio properties ...
}
```

Async data sources can return row-based or column-based data. This is determined by the `dataShape` property.

Properties available on the `AgDataSourceDefinition&lt;TDataShape extends AgDataShape, TRegistry extends AgBaseRegistry = AgDefaultRegistry&gt;` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | Data source ID |
| `name` | `string` |  | Data source display name. If not provided, a formatted version of `id` will be used. |
| `getData` | `Function` |  | Callback to return the data for the provided table and fields. The optional `options` parameter carries a `paging` row-window hint, a `filter` tree, and a `sort` order. The engine only includes `paging` when the source declares `capabilities.pagination: true`, and only includes `filter`/`sort` when the source declares `capabilities.filter: true`/`capabilities.sort: true` respectively - independently of `capabilities.pagination`. Sources that do not declare a given capability receive no corresponding option and should return all data for that aspect - the engine applies client-side pagination, filtering, and sorting as a fallback. |
| `dataShape` | `TDataShape` |  | `'row'` if the data is row-based, or `'column'` if the data is column-based. |
| `tables` | `AgAsyncTableDefinition<TRegistry>[]` |  | One or more tables that are provided by this data source. |
| `capabilities` | `AgDataSourceCapabilities` |  | Static capability declaration. Tells the engine whether the `getData` callback supports server-side pagination. When omitted, the engine treats the source as having no server-side capabilities and applies client-side pagination as a fallback. |
| `pageSize` | `number` |  | Maximum number of rows the engine requests from this source in a single `getData` call. Set this when the source has a real per-call limit it cannot exceed (for example, a REST API with its own hard page-size cap). Whenever the engine needs more rows than this from the source - including an unrestricted "fetch everything" request - it issues multiple `getData` calls of at most this many rows each and assembles the combined result, rather than trusting a single call to return more than the source can actually serve. Only has an effect when `capabilities.pagination` is `true`. Applies to both row-shape and column-shape sources - for a column-shape source, each field's column is fetched and reassembled in `pageSize`-sized chunks the same way row data is. |

### Reloading Async Data

Async data can be reloaded by calling `api.reload()`.

#### Reloading Async Data

```ts
import {
  AgFieldDefinition,
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
  enableStudioDevValidations,
} from "ag-studio";

if (process.env.NODE_ENV !== "production") {
  // Enable extended validations only for development
  enableStudioDevValidations();
}

const initialState: AgReportState = {
  pages: [
    {
      id: "page1",
      widgets: {
        "1": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "medals.country" },
              { id: "medals.sport" },
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
            ],
          },
        },
        "2": {
          type: "column-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "medals.country" }],
            valueKey: [
              { id: "medals.gold", aggregation: "sum" },
              { id: "medals.silver", aggregation: "sum" },
              { id: "medals.bronze", aggregation: "sum" },
            ],
            tooltipKey: [],
          },
        },
      },
      widgetLayout: {
        "1": {
          xTrack: 0,
          yTrack: 0,
          xSpan: 24,
          ySpan: 16,
        },
        "2": {
          xTrack: 0,
          yTrack: 16,
          xSpan: 24,
          ySpan: 16,
        },
      },
    },
  ],
  selectedPageId: "page1",
  panels: {
    filters: {
      collapsed: true,
    },
    edit: {
      collapsed: true,
    },
  },
};

const fields: AgFieldDefinition[] = [
  {
    id: "athlete",
    format: "textFormat",
  },
  {
    id: "country",
    format: "textFormat",
  },
  {
    id: "year",
    format: "integerFormat",
    formatOptions: { format: "0" },
  },
  {
    id: "sport",
    format: "textFormat",
  },
  {
    id: "gold",
    format: "integerFormat",
  },
  {
    id: "silver",
    format: "integerFormat",
  },
  {
    id: "bronze",
    format: "integerFormat",
  },
];

async function loadData(): Promise<Record<string, any>[]> {
  const response = await fetch(
    "https://www.ag-grid.com/studio/archive/3.0.0/example-assets/olympic-winners.json",
  );
  const sourceData = await response.json();
  return sourceData
    .slice(
      Math.floor(window.agRandom() * 100),
      200 + Math.floor(window.agRandom() * 100),
    )
    .map((row: any) => ({
      ...row,
      gold: Math.floor(window.agRandom() * 3),
      silver: Math.floor(window.agRandom() * 4),
      bronze: Math.floor(window.agRandom() * 4),
    }));
}

let dataPromise: Promise<Record<string, any>[]> = loadData();

function reload() {
  dataPromise = loadData();
  studioApi.reload();
}

const studioProperties: AgStudioProperties = {
  mode: "edit",
  initialState,
  data: {
    sources: [
      {
        id: "medalsSource",
        dataShape: "row",
        getData: async () => ({ data: await dataPromise }),
        tables: [
          {
            id: "medals",
            fields,
          },
        ],
      },
    ],
  },
};

let studioApi: AgStudioApi;

// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);

if (typeof window !== "undefined") {
  // Attach external event handlers to window so they can be called from index.html
  (<any>window).reload = reload;
}
```

[Live example: Reloading Async Data](https://www.ag-grid.com/studio/archive/3.0.0/examples/loading-data/async-data-reload/typescript/)

### Data Source Filtering, Sorting, and Pagination

A source declares `capabilities.filter`, `capabilities.sort`, and `capabilities.pagination` independently of each other - any combination of the three can be set. A source without `capabilities.pagination` is still re-fetched in full whenever the applied filter or sort changes, rather than served from a single cached fetch, so declaring `filter`/`sort` there still avoids evaluating that criteria locally, just without the row-window benefit `pagination` also provides.

The example below simulates a server-side endpoint that handles all three. Its request log prints the exact filter, sort order and row window sent to the fake server on every call.

The example starts with a page filter on `Region` already applied, so the log shows an unfiltered fetch of all 240 rows and then the filtered one, which the server narrows to 120 before returning anything. The filter arrives as a tree naming the field and operator, not as pre-resolved rows. Change it in the Filters panel, or add one on `Amount`, to watch the requests change.

The source also declares a `pageSize` of 100, so each of those fetches is split into consecutive calls with advancing row windows rather than one request for the whole result.

#### Filter, Sort & Pagination Pushdown

```ts
import {
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  createStudio,
  enableStudioDevValidations,
} from "ag-studio";
import { PAGE_SIZE, TABLE_ID, createFakeServer, fields } from "./data.ts";

if (process.env.NODE_ENV !== "production") {
  // Enable extended validations only for development
  enableStudioDevValidations();
}

const initialState: AgReportState = {
  pages: [
    {
      id: "page1",
      widgets: {
        "1": {
          type: "grid",
          dataMapping: {
            cols: [
              { id: "orders.region" },
              { id: "orders.product" },
              { id: "orders.amount", aggregation: "sum" },
              { id: "orders.units", aggregation: "sum" },
            ],
          },
        },
      },
      widgetLayout: {
        "1": {
          xTrack: 0,
          yTrack: 0,
          xSpan: 24,
          ySpan: 32,
        },
      },
      // Applied up front so the log shows an unfiltered fetch and a filtered one together,
      // rather than needing the reader to add a filter before anything is comparable.
      filter: {
        page: [
          {
            field: { id: "orders.region" },
            view: { expanded: true },
            model: {
              operator: "isIn",
              value: ["North", "South"],
            },
          },
        ],
      },
    },
  ],
  selectedPageId: "page1",
  panels: {
    filters: {
      collapsed: false,
    },
    edit: {
      collapsed: true,
    },
  },
};

const studioProperties: AgStudioProperties = {
  mode: "edit",
  initialState,
  data: {
    sources: [
      {
        id: "ordersSource",
        dataShape: "row",
        // Each is independent: the engine sends `filter`, `sort` and `paging` only to a
        // source claiming the matching capability, and trusts the response as already done.
        capabilities: { filter: true, sort: true, pagination: true },
        // Any request above this cap - including the unwindowed fetch a grouping widget
        // issues - splits into consecutive `getData` calls of at most PAGE_SIZE rows.
        pageSize: PAGE_SIZE,
        getData: createFakeServer(),
        tables: [
          {
            id: TABLE_ID,
            fields,
          },
        ],
      },
    ],
  },
};

let studioApi: AgStudioApi;

// setup Studio after the page has finished loading
const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);
```

[Live example: Filter, Sort & Pagination Pushdown](https://www.ag-grid.com/studio/archive/3.0.0/examples/loading-data/async-data-pushdown/typescript/)

The grid in this example groups by `Region` and `Product` and aggregates `Amount` and `Units`, so it sorts and pages that grouped result locally: the log reports `sort=none` however the grid is sorted, and the row windows it does show come from `pageSize` splitting one full fetch. That is what a grouping query does, not a missing sort - see [Sort Pushdown](https://www.ag-grid.com/studio/archive/3.0.0/javascript/loading-data/#sort-pushdown) below.

#### Filter Pushdown

By default, filters applied in AG Studio are evaluated locally after fetching all the data. A source that sets `capabilities.filter: true` receives the applied filter as part of `getData`'s `options`, and can filter the data itself before returning it.

```ts
{
    capabilities: { filter: true },
    getData: async (tableId, fieldIds, options) => {
        const rows = await fetchRows(tableId, options?.filter);
        return { data: rows };
    },
}
```

A filter on an aggregated value (a measure, or a total) is always evaluated locally as the aggregates they filter on are only available locally.

#### Sort Pushdown

By default, sorting applied in AG Studio is evaluated locally after fetching all the data. A source that sets `capabilities.sort: true` receives the applied sort order as part of `getData`'s `options`, and can sort the data itself before returning it.

```ts
{
    capabilities: { sort: true },
    getData: async (tableId, fieldIds, options) => {
        const rows = await fetchRows(tableId, options?.sort);
        return { data: rows };
    },
}
```

Sort pushdown, and the row-window benefit of pagination, apply to a query that reads source rows directly. Where a query groups or aggregates its fields, it resolves its sort order and row window over that computed result instead, and both are applied locally whatever the source declares.

Whether a particular widget's query groups or aggregates depends on how that widget is configured, so read the requests your own source receives to see which of them carry a `sort` or a `paging` window.

#### Pagination

By default, a query's full result set is fetched in one call and paged through locally. A source that sets `capabilities.pagination: true` instead receives a `paging.offset`/`paging.pageLimit` window as part of `getData`'s `options`, and is expected to return exactly that window - the engine trusts the response as-is, with no local re-slicing, other than truncating a response that returns more rows than the requested `pageLimit`.

```ts
{
    capabilities: { pagination: true },
    pageSize: 500,
    getData: async (tableId, fieldIds, options) => {
        const { offset = 0, pageLimit } = options?.paging ?? {};
        const rows = await fetchRows(tableId, offset, pageLimit);
        return { data: rows };
    },
}
```

Set `pageSize` on the source when it has its own per-call row limit (a REST API's page-size cap, for example). The engine then splits any request for more rows than `pageSize` - including an unbounded "fetch everything" request - into multiple `getData` calls of at most `pageSize` rows each, and assembles the combined result. `pageSize` only has an effect alongside `capabilities.pagination: true`.

> **Note**
>
> A response with fewer rows than requested is always read as "no more data after this point".

## Multiple Tables

When multiple tables are provided, whether sync or async, they can be linked by providing [Relationships](https://www.ag-grid.com/studio/archive/3.0.0/javascript/data-modelling/#relationships).
