---
title: "Row Grouping - Editing Groups"
enterprise: true
framework: javascript
version: "36.1.0"
---

# Row Grouping - Editing Groups

The grid supports editing grouped data when using the [Client-Side Row Model](https://www.ag-grid.com/javascript-data-grid/row-models/#client-side). This page covers making group row cells editable, distributing edited values to descendant rows, and refreshing the grouping hierarchy after edits.

## Editing Group Row Cells

Set `groupRowEditable` on any column to make its group row cells editable. The grid then distributes the edited value to descendant rows using the built-in distribution. To customise this, set `groupRowValueSetter` to an options object, a callback, or `false` to disable distribution.

In the example below, double-click any group row's `Sales` cell to edit it. The new total spreads equally across the group's children, recursing through nested groups.

#### Built-in Sum Distribution

```ts
import {
  ClientSideRowModelModule,
  GridApi,
  GridOptions,
  ModuleRegistry,
  NumberEditorModule,
  NumberFilterModule,
  TextFilterModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import { RowGroupingEditModule, RowGroupingModule } from "ag-grid-enterprise";
import { SalesRecord, getData } from "./data";

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

ModuleRegistry.registerModules([
  RowGroupingModule,
  RowGroupingEditModule,
  ClientSideRowModelModule,
  NumberEditorModule,
  NumberFilterModule,
  TextFilterModule,
]);

let gridApi: GridApi<SalesRecord>;

const gridOptions: GridOptions<SalesRecord> = {
  columnDefs: [
    { field: "region", rowGroup: true, hide: true },
    { field: "country", rowGroup: true, hide: true },
    {
      headerName: "Sales",
      field: "sales",
      aggFunc: "sum",
      editable: true,

      // Enable editing on group rows and use the built-in distribution.
      // With 'sum' aggregation, the default strategy is 'uniform' — the new total
      // is divided equally among all children.
      // precision: 0 rounds values to integers and spreads the rounding remainder
      // across children so the total matches exactly.
      groupRowEditable: true,
      groupRowValueSetter: {
        precision: 0,
      },
    },
  ],
  autoGroupColumnDef: {
    minWidth: 220,
    cellRendererParams: { suppressCount: true },
  },
  defaultColDef: {
    flex: 1,
    sortable: true,
    filter: true,
    resizable: true,
  },
  rowData: getData(),
  groupDefaultExpanded: -1,
  getRowId: ({ data }) => data.id,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
```

[Live example: Built-in Sum Distribution](https://www.ag-grid.com/examples/grouping-edit/distribute-group-value-sum/typescript)

`groupRowEditable` accepts a `boolean` or a callback. The callback runs only for group rows (`rowNode.group === true`); leaf rows continue to honour `editable`. When a column sets both properties, the grid uses whichever matches the current row type, allowing separate rules for group rows and leaf rows.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `groupRowEditable` | `boolean \| GroupRowEditableCallback` |  |  | Works like `editable`, but is evaluated only for group rows. When provided, group rows use this property instead of `editable`. Set to `true` to make group row cells editable, or use a callback to control editability per row. When `groupRowEditable` is defined and no explicit `groupRowValueSetter` is provided, the built-in `distributeGroupValue` (exported from `ag-grid-enterprise`) is used automatically. Columns with `groupRowEditable` or `groupRowValueSetter` do not require `field` or `valueSetter` - the group row value setter handles the edit entirely. Note: if `groupRowValueSetter` resolves to `false` or `null` (via `distribution: false`, a per-aggFunc record entry, or `groupRowValueSetter: false`), the cell is treated as not editable even when `groupRowEditable` is `true`. Module: [`RowGroupingEditModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |
| `groupRowValueSetter` | `boolean \| GroupRowValueSetterFunc \| GroupRowValueSetterOptions` |  |  | Controls how a group row value edit is distributed to descendant rows. - **`true`**: Uses the built-in `distributeGroupValue` (exported from `ag-grid-enterprise`) with default settings. Also enabled implicitly when `groupRowEditable` is defined and `groupRowValueSetter` is not set. - **`false`**: Explicitly disables group row value distribution and makes the cell not editable, even if `groupRowEditable` is defined. - **Function**: A custom callback that receives a GroupRowValueSetterParams and pushes edits down to descendants. The column does not need `field` or `valueSetter` - the callback handles the edit entirely. - **Options object**: Uses the built-in distribution logic with a GroupRowValueSetterOptions configuration. When `distribution` resolves to `false` or `null` for the column's aggFunc, the cell is treated as not editable (overriding `groupRowEditable`). Fires for every `setDataValue` call when active, regardless of `groupRowEditable`. Module: [`RowGroupingEditModule`](https://www.ag-grid.com/javascript-data-grid/modules/). |

## Built-in Value Distribution

The built-in distribution adjusts each child so the column's `aggFunc` produces the new total. Pass an options object to `groupRowValueSetter` to customise the strategy or precision.

```js
const gridOptions = {
    columnDefs: [
        {
            field: 'sales',
            aggFunc: 'sum',
            editable: true,
            groupRowEditable: true,
            // Built-in distribution: divides the sum equally among children.
            // precision: 0 rounds child values to integers and spreads the remainder.
            groupRowValueSetter: { precision: 0 },
        },
    ],

    // other grid options ...
}
```

### Distribution Strategies

The `distribution` option controls how the edited value is spread across children. When omitted, the strategy is chosen automatically based on the column's `aggFunc`:

| Strategy | Behaviour | Default for |
| --- | --- | --- |
| `'uniform'` | Divides the new value equally among all children. | `sum` |
| `'overwrite'` | Writes the new value directly to every child. | `avg`, no aggFunc |
| `'percentage'` | Scales each child proportionally, preserving relative weights. Falls back to `'uniform'` when the current total is zero. | — |
| `'increment'` | Distributes only the difference (`newValue − oldValue`) among children, adding it to current values. | — |

Editing is **disabled by default** for `count`, `min`, `max`, `first`, `last`, and **custom aggregation functions**. These functions have no unique inverse, so the grid cannot choose a safe default. To enable editing for them:

- Set `distribution` to `'overwrite'` to write the new value directly to every child.
- Provide a per-aggFunc record entry with `'overwrite'`, `true`, or a custom callback (see [Per-Aggregation Strategies](#per-aggregation-strategies)).
- Assign a [callback](#custom-distribution-with-a-callback) to `groupRowValueSetter` for full control.

```js
const gridOptions = {
    columnDefs: [
        {
            field: 'price',
            aggFunc: 'min',
            editable: true,
            groupRowEditable: true,
            // 'overwrite' writes the edited value to every child
            groupRowValueSetter: { distribution: 'overwrite' },
        },
        {
            field: 'quantity',
            aggFunc: 'count',
            editable: true,
            groupRowEditable: true,
            // Per-aggFunc record: enable only count with overwrite
            groupRowValueSetter: { distribution: { count: 'overwrite' } },
        },
    ],

    // other grid options ...
}
```

Setting `distribution` to `true` uses built-in defaults for `sum`, `avg`, no-aggFunc columns, and custom aggregation functions. The `count`, `min`, `max`, `first`, and `last` functions stay disabled even with `true`; use `'overwrite'`, a per-aggFunc record entry, or a callback to enable them.

Setting `distribution` to `false` or `null` suppresses distribution and makes the cell not editable, even when `groupRowEditable` is `true`.

For `avg`, every strategy adjusts child values so the children's average equals the edited value.

> **Note**
>
> The `precision` option rounds the values written to **child rows**, not the group row's displayed value. The group value is re-computed by the column's `aggFunc` from the rounded children. For `sum` this is always exact. For `avg` the re-aggregated value may differ: distributing `precision: 0` across 3 children produces integers, but their average (`10 / 3 ≈ 3.33`) is not. Use a [Value Formatter](https://www.ag-grid.com/javascript-data-grid/value-formatters/) to control how the group row displays the re-aggregated value.

### Distribution Options

Properties available on the `GroupRowValueSetterOptions&lt;TData = any, TValue = any, TContext = any&gt;` interface.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `distribution` | `Distribution` |  |  | Distribution strategy or per-aggregation-function strategy map. **As a string:** applies the chosen GroupRowValueSetterDistribution strategy to all aggregation functions. **As `true`:** enables distribution using built-in defaults for distributable aggregation functions and custom aggFuncs (which get `'overwrite'`). Note: `count`/`min`/`max`/`first`/`last` are only enabled via explicit per-aggFunc record entries. Useful for overriding `false`/`null` from `defaultColDef` in deep-merge scenarios. **As `false` or `null`:** suppresses distribution and makes the cell not editable (overriding `groupRowEditable`). **As a record:** maps aggFunc names to individual strategies, options objects, or custom callbacks. Unmatched aggFuncs fall through to default, then to the built-in defaults. Example // Single strategy distribution: 'percentage' // Enable all aggFuncs with built-in defaults distribution: true // Per-aggFunc record (entries can be strings, objects, functions, true, or false/null) distribution: { sum: 'percentage', avg: 'increment', count: true, myAgg: (params) =&gt; { ... }, min: false } |
| `default` | `GroupRowValueSetterDistributionEntry` |  |  | Fallback for aggFuncs that don't have a specific distribution strategy. When `distribution` is a record, applies to aggFuncs not listed in the record. When `distribution` is omitted, applies only to custom (non-built-in) aggFuncs. Ignored when `distribution` is a string (all aggFuncs use the specified strategy). Accepts the same values as record entries: A function for full custom handling. A strategy string (e.g. `'overwrite'`). `false` or `null` to suppress distribution and make unmatched aggFunc cells not editable. An options object with strategy and precision. Example // Custom handler default: (params) =&gt; { for (const child of params.aggregatedChildren) { child.setDataValue(params.column, params.newValue, 'data'); } } // Suppress all unmatched aggFuncs default: false |
| `precision` | `number \| false` |  |  | Number of decimal places to round values written to **child rows** during distribution. Spreads any rounding remainder across children so their total matches exactly. `0` — integers (e.g. `10 / 3` → `[4, 3, 3]`) `2` — two decimals (e.g. `10 / 3` → `[3.34, 3.33, 3.33]`) `false` — disable rounding (overrides auto-detect) `undefined` (default) — auto-detect from the column definition: `cellEditorParams.precision` if set, `0` if `cellEditorParams.step` is a whole number, no rounding otherwise. Note: the group row's displayed value is re-computed by the `aggFunc` after distribution. For `sum`, the sum of rounded children always honours the same precision. For other aggregation functions like `avg`, the re-aggregated value may not — for example, the average of integers is not necessarily an integer. Ignored for `bigint` columns — bigint values are always distributed as integers. Example // Round child values to integers colDef.groupRowValueSetter = { precision: 0 }; // Round child values to 2 decimal places (e.g. currency) colDef.groupRowValueSetter = { precision: 2 }; |
| `getValue` | `Function` |  |  | Reads a child's current value during distribution. Default: `node.getDataValue(column, 'value')`. Override to read from a custom data structure or computed field. Returns: The child's current value. Example getValue: (params) =&gt; params.data?.weight ?? 0, |
| `setValue` | `Function` |  |  | Writes a distributed value to a child. Default: `node.setDataValue(column, value, 'data')`. Override to write to a custom data structure or apply transformations. Returns: `true` if the value was changed, `false` otherwise. Example setValue: (params) =&gt; // Apply a minimum of 0 before writing params.node.setDataValue(params.column, Math.max(0, Number(params.value)), 'data'), |

### Merging with Default Column Definitions

When `groupRowValueSetter` is set as an options object on both `defaultColDef` and a column, the grid deep merges the two. The column's properties take precedence; anything not specified on the column is inherited from the default. This includes nested `distribution` records.

```js
const gridOptions = {
    defaultColDef: {
        editable: true,
        groupRowEditable: true,
        // Default: round child values to integers, disable count
        groupRowValueSetter: { precision: 0, distribution: { count: false } },
    },
    columnDefs: [
        {
            field: 'sales',
            aggFunc: 'sum',
            // Column-level override: use percentage distribution.
            // precision: 0 is inherited from defaultColDef.
            groupRowValueSetter: { distribution: 'percentage' },
        },
        {
            field: 'quantity',
            aggFunc: 'count',
            // Override defaultColDef's false with true to enable editing for count.
            groupRowValueSetter: { distribution: { count: true } },
        },
        {
            field: 'profit',
            aggFunc: 'sum',
            // No column-level override — inherits { precision: 0 } from defaultColDef.
        },
    ],

    // other grid options ...
}
```

> **Note**
>
> Deep merging only applies when both the `defaultColDef` and the column define `groupRowValueSetter` as plain objects. If the column defines a callback function, it replaces the default entirely.

### Multiple Aggregation Functions

The following example mixes built-in strategies, a non-aggregated column, and custom callbacks. Double-click any group row cell to edit it:

- **Salary** (`sum`): divides the new total equally among children, rounded to integers (`precision: 0`). Editing 300 across 3 children produces 100 each.
- **Bonus** (`avg`): overwrites every child with the edited value. Editing the average to 15 sets all children's bonus to 15.
- **Projects** (no `aggFunc`): overwrites every child with the edited value. The group cell is blank but still editable.
- **Rate %** (custom callback): computes each leaf child's bonus as `salary × rate / 100`. Entering 20 sets each child's bonus to 20% of their salary. Uses `node.getAggregatedChildren(column, true)` to reach all descendant leaf rows directly.
- **SumSq** (custom `sumOfSquares` aggFunc): a callback sets each child to `√(newValue / count)`, the inverse of sum-of-squares.

#### Multiple Aggregation Functions

```ts
import {
  ClientSideRowModelModule,
  GridApi,
  GridOptions,
  GroupRowValueSetterFunc,
  IAggFunc,
  IAggFuncParams,
  ModuleRegistry,
  NumberEditorModule,
  NumberFilterModule,
  TextEditorModule,
  TextFilterModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import { RowGroupingEditModule, RowGroupingModule } from "ag-grid-enterprise";
import { MetricsRecord, getData } from "./data";

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

ModuleRegistry.registerModules([
  RowGroupingModule,
  RowGroupingEditModule,
  ClientSideRowModelModule,
  NumberEditorModule,
  TextEditorModule,
  NumberFilterModule,
  TextFilterModule,
]);

let gridApi: GridApi<MetricsRecord>;

/** Custom aggregation function: sum of squares. */
const sumOfSquares: IAggFunc = (params: IAggFuncParams) => {
  let total = 0;
  for (const value of params.values) {
    const n = Number(value);
    if (Number.isFinite(n)) {
      total += n * n;
    }
  }
  return total;
};

/**
 * Custom groupRowValueSetter for sumOfSquares.
 *
 * Because sumOfSquares uses x² in its aggregation, the built-in strategies
 * (uniform, percentage, etc.) would not produce correct results.
 * Instead, we compute the square root to find the per-child value that
 * produces the desired aggregate: each child = √(newValue / count).
 */
const sumOfSquaresValueSetter: GroupRowValueSetterFunc<MetricsRecord> = ({
  newValue,
  aggregatedChildren,
  column,
}) => {
  const target = Number(newValue);
  const count = aggregatedChildren.length;
  if (!count || !Number.isFinite(target)) {
    return false;
  }
  const perChild = Math.round(Math.sqrt(Math.max(0, target / count)));
  let changed = false;
  for (const child of aggregatedChildren) {
    if (child.setDataValue(column, perChild, "data")) {
      changed = true;
    }
  }
  return changed;
};

/**
 * Cross-column groupRowValueSetter: editing the group row's "Rate %"
 * sets each leaf child's bonus to a percentage of their individual salary.
 *
 * Uses getAggregatedChildren with recursive=true to reach all descendant
 * leaf rows, since intermediate group rows don't have salary data.
 *
 * For example, entering 20 on the Engineering group sets each engineer's
 * bonus to 20% of their salary: Alice (salary 90) gets bonus 18,
 * Dave (salary 95) gets bonus 19, etc.
 */
const bonusRateSetter: GroupRowValueSetterFunc<MetricsRecord> = ({
  newValue,
  node,
  column,
}) => {
  const rate = Number(newValue) / 100;
  if (!Number.isFinite(rate)) {
    return false;
  }
  const leaves = node.getAggregatedChildren(column, true);
  if (!leaves.length) {
    return false;
  }
  let changed = false;
  for (const child of leaves) {
    const salary = child.data?.salary ?? 0;
    if (child.setDataValue("bonus", Math.round(salary * rate), "data")) {
      changed = true;
    }
  }
  return changed;
};

const gridOptions: GridOptions<MetricsRecord> = {
  columnDefs: [
    { field: "department", rowGroup: true, hide: true },
    { field: "team", rowGroup: true, hide: true },
    { field: "employee", minWidth: 120 },

    // sum: uniform distribution (divides equally), with integer rounding
    {
      field: "salary",
      aggFunc: "sum",
      groupRowValueSetter: { precision: 0 },
    },

    // avg: default strategy is 'overwrite' — sets every child to the edited value
    {
      field: "bonus",
      aggFunc: "avg",
      valueFormatter: ({ value }) =>
        value != null ? Number(value).toFixed(2) : "",
    },

    // No aggFunc: the default strategy is 'overwrite', writing the edited
    // value to every child. The group cell is blank (no aggregation) but
    // still editable via groupRowEditable.
    { field: "projects" },

    // Cross-column custom callback: editing the group row's "Rate"
    // reads each child's salary and writes a computed bonus.
    {
      headerName: "Rate %",
      valueGetter: ({ data }) =>
        data ? (data.bonus / data.salary) * 100 : null,
      valueFormatter: ({ value }) =>
        value != null ? Number(value).toFixed(2) : "",
      aggFunc: "avg",
      editable: false,
      groupRowEditable: true,
      groupRowValueSetter: bonusRateSetter,
    },

    // Custom aggregation function with a custom groupRowValueSetter
    {
      headerName: "SumSq",
      field: "score",
      aggFunc: "sumOfSquares",
      groupRowValueSetter: sumOfSquaresValueSetter,
    },
  ],
  defaultColDef: {
    flex: 1,
    minWidth: 120,
    sortable: true,
    filter: true,
    resizable: true,

    // Enable editing on all columns (leaf rows and group rows).
    // When groupRowEditable is defined, the built-in distribution
    // automatically uses the right strategy for each aggregation function.
    editable: true,
    groupRowEditable: true,
  },
  aggFuncs: {
    sumOfSquares,
  },
  autoGroupColumnDef: {
    minWidth: 200,
    cellRendererParams: { suppressCount: true },
  },
  rowData: getData(),
  groupDefaultExpanded: -1,
  getRowId: ({ data }) => data.id,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
```

[Live example: Multiple Aggregation Functions](https://www.ag-grid.com/examples/grouping-edit/distribute-group-value-all-agg/typescript)

### Per-Aggregation Strategies

When several columns share a `groupRowValueSetter` (e.g. via `defaultColDef`) but use different aggregation functions, set `distribution` to a record keyed by `aggFunc` name. Each entry can be a strategy string, an options object, a custom callback, `true` for built-in defaults, or `false`/`null` to suppress distribution for that aggFunc:

```js
const myCustomAgg = (params) => {
    let total = 0;
    for (const value of params.values) {
        total += Number(value) || 0;
    }
    return total;
};

// Optional: inverse enables incremental aggregation on row changes
myCustomAgg.inverse = (params) => {
    let total = Number(params.result) || 0;
    for (const value of params.values) {
        total -= Number(value) || 0;
    }
    return total;
};

const gridOptions = {
    aggFuncs: {
        myCustomAgg,
    },
    defaultColDef: {
        editable: true,
        groupRowEditable: true,
        groupRowValueSetter: {
            precision: 0,
            distribution: {
                sum: 'percentage',                          // strategy string
                avg: { distribution: 'increment' },         // options object
                myCustomAgg: (params) => {                  // custom callback function
                    for (const child of params.aggregatedChildren) {
                        child.setDataValue(params.column, params.newValue, 'data');
                    }
                    return true;
                },
            },
        },
    },

    // other grid options ...
}
```

Custom aggregation functions are disabled by default. To enable editing, add an explicit entry in the `distribution` record. The entry can be a strategy string, an options object, or a callback that implements the inverse of the aggregation logic:

```js
const gridOptions = {
    defaultColDef: {
        editable: true,
        groupRowEditable: true,
        groupRowValueSetter: {
            precision: 0,
            distribution: {
                sum: 'percentage',
                avg: 'increment',
                count: 'overwrite',
                // Custom aggFunc: provide a callback that reverses the aggregation
                myCustomAgg: (params) => {
                    for (const child of params.aggregatedChildren) {
                        child.setDataValue(params.column, params.newValue, 'data');
                    }
                    return true;
                },
            },
        },
    },

    // other grid options ...
}
```

Aggregation functions not listed in the record fall through to the `default` fallback, then to the built-in defaults (see the strategy table above).

### Default Fallback

When multiple custom aggregation functions share the same distribution logic, the `default` property avoids repeating entries for each one. It applies to any aggregation function not explicitly listed in the `distribution` record, including custom aggregations (which are otherwise disabled) and columns with no `aggFunc` (which already default to `'overwrite'`). Non-distributable functions (`count`, `min`, `max`, `first`, `last`) are **not** affected by `default` and must always be listed explicitly.

```js
const gridOptions = {
    defaultColDef: {
        editable: true,
        groupRowEditable: true,
        groupRowValueSetter: {
            precision: 0,
            distribution: {
                sum: 'percentage',
                count: 'overwrite',
            },
            // Fallback for unlisted custom aggFuncs and no-aggFunc columns.
            default: 'overwrite',
        },
    },

    // other grid options ...
}
```

### Custom getValue and setValue

The `getValue` and `setValue` callbacks override how the built-in distributor reads from and writes to child rows. By default it uses `node.getDataValue(column, 'value')` to read and `node.setDataValue(column, value, 'data')` to write. Provide your own when child values live on a different field or custom data structure, when you need to apply transformations or side effects during distribution, or when the column's `valueGetter` / `valueSetter` are not suitable for distribution.

```js
const gridOptions = {
    columnDefs: [
        {
            field: 'amount',
            aggFunc: 'sum',
            editable: true,
            groupRowEditable: true,
            groupRowValueSetter: {
                distribution: 'percentage',
                getValue: (params) => (params.data && params.data.weight) || 0,
                setValue: (params) => params.node.setDataValue(params.column, params.value, 'data'),
            },
        },
    ],

    // other grid options ...
}
```

## Custom Distribution with a Callback

For full control over how group-level edits cascade, assign a function to `groupRowValueSetter`. The callback receives a `GroupRowValueSetterParams` object and should return `true` when any child value was changed.

The example below distributes the edited total equally among children, rounded to integers (largest remainder method). It calls `setDataValue` on each child; aggregation then refreshes parent totals automatically.

#### Custom Editable Group Totals

```ts
import {
  ClientSideRowModelModule,
  GridApi,
  GridOptions,
  GroupRowValueSetterFunc,
  ModuleRegistry,
  NumberFilterModule,
  TextEditorModule,
  ValueParserParams,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingEditModule,
  RowGroupingModule,
  SetFilterModule,
} from "ag-grid-enterprise";
import { getData } from "./data";

let gridApi: GridApi<SalesRecord>;

interface SalesRecord {
  id: string;
  region: string;
  segment: string;
  country: string;
  amount: number;
}

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

ModuleRegistry.registerModules([
  RowGroupingModule,
  RowGroupingEditModule,
  ClientSideRowModelModule,
  NumberFilterModule,
  SetFilterModule,
  TextEditorModule,
]);

// Parse input to integer
const amountValueParser = (params: ValueParserParams): number | null => {
  const numericValue = Number(params.newValue);
  return Number.isFinite(numericValue)
    ? Math.round(numericValue)
    : (params.oldValue ?? null);
};

/**
 * Distributes a new group total equally among children.
 *
 * `aggregatedChildren` contains the immediate children used for aggregation:
 * - For leaf groups: the data rows
 * - For non-leaf groups: the child groups
 *
 * Calling `setDataValue` on a child group triggers `groupRowValueSetter` again,
 * enabling recursive cascade through the entire group hierarchy.
 */
const cascadeGroupTotal: GroupRowValueSetterFunc<SalesRecord> = ({
  column,
  newValue,
  eventSource,
  aggregatedChildren,
}) => {
  const total = Number(newValue);
  if (!Number.isFinite(total) || !aggregatedChildren.length) {
    return false;
  }

  // Distribute equally among children
  // https://en.wikipedia.org/wiki/Largest_remainder_method
  const count = aggregatedChildren.length;
  const base = Math.floor(total / count);
  let remainder = Math.round(total) - base * count;

  // Apply the distributed values
  let changed = false;
  for (const child of aggregatedChildren) {
    let value = base;
    if (remainder > 0) {
      value++;
      remainder--;
    }
    if (child.setDataValue(column, value, eventSource)) {
      changed = true;
    }
  }
  return changed;
};

const gridOptions: GridOptions<SalesRecord> = {
  columnDefs: [
    { field: "region", rowGroup: true, hide: true },
    {
      field: "segment",
      rowGroup: true,
      hide: true,
      filter: "agSetColumnFilter",
    },
    { field: "country", filter: "agSetColumnFilter" },
    {
      headerName: "Amount",
      field: "amount",
      aggFunc: "sum",
      editable: true,
      groupRowEditable: true,
      filter: "agNumberColumnFilter",
      valueParser: amountValueParser,
      groupRowValueSetter: cascadeGroupTotal,
    },
  ],
  autoGroupColumnDef: {
    minWidth: 260,
    cellRendererParams: {
      suppressCount: true,
    },
  },
  defaultColDef: {
    flex: 1,
    sortable: true,
    filter: true,
    resizable: true,
  },
  rowData: getData(),
  groupAggFiltering: true,
  groupDefaultExpanded: -1,
  animateRows: true,
  getRowId: ({ data }) => data.id,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
```

[Live example: Custom Editable Group Totals](https://www.ag-grid.com/examples/grouping-edit/group-editable-totals-custom/typescript)

### GroupRowValueSetterParams

Properties available on the `GroupRowValueSetterParams&lt;TData = any, TValue = any, TContext = any&gt;` interface.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `api` | [`GridApi`](https://www.ag-grid.com/javascript-data-grid/grid-api/) |  |  | The grid api. |
| `context` | [`TContext`](https://www.ag-grid.com/javascript-data-grid/typescript-generics/#context-tcontext) |  |  | Application context as set on `gridOptions.context`. |
| `column` | [`Column`](https://www.ag-grid.com/javascript-data-grid/column-object/) |  |  | Column for this callback. |
| `colDef` | [`ColDef`](https://www.ag-grid.com/javascript-data-grid/column-properties/) |  |  | ColDef provided for this column. |
| `oldValue` | [`TValue \| null \| undefined`](https://www.ag-grid.com/javascript-data-grid/typescript-generics/#cell-value-tvalue) |  |  | The value before the change. |
| `newValue` | [`TValue \| null \| undefined`](https://www.ag-grid.com/javascript-data-grid/typescript-generics/#cell-value-tvalue) |  |  | The value after the change. |
| `node` | [`IRowNode`](https://www.ag-grid.com/javascript-data-grid/row-object/) |  |  | The group row node being edited. |
| `data` | [`TData \| null \| undefined`](https://www.ag-grid.com/javascript-data-grid/typescript-generics/#row-data-tdata) |  |  | Row data for the group node. `null` or `undefined` for grouping groups or tree data filler nodes. |
| `eventSource` | `string \| undefined` |  |  | What triggered the edit (e.g. `'ui'`, `'undo'`, `'paste'`). |
| `valueChanged` | `boolean` |  |  | Whether the aggregated value actually changed compared to the previous value. |
| `aggregatedChildren` | [`IRowNode[]`](https://www.ag-grid.com/javascript-data-grid/row-object/) |  |  | The immediate children that contribute to this group's aggregation. **Leaf groups** (groups directly containing data rows): the data rows themselves. **Non-leaf groups** (groups containing sub-groups): the child group rows. Calling `setDataValue()` on a child group cascades the edit recursively through the full hierarchy. The built-in `distributeGroupValue` does this automatically. **Pivot mode**: only rows matching the edited pivot column's keys are included. Use rowNode.getAggregatedChildren(colKey) to retrieve the same children programmatically. Pass `true` as the second argument to collect all descendant leaf rows recursively. Only supported with the Client-Side Row Model. |

### Aggregated Children

The `groupRowValueSetter` callback receives an `aggregatedChildren` array: the immediate children that contribute to the [Aggregation](https://www.ag-grid.com/javascript-data-grid/aggregation/) for the edited column. Cascading edits to these children avoids traversing the row hierarchy by hand.

What `aggregatedChildren` returns depends on the column and grid configuration:

- **Regular value columns**: the direct children used for aggregation. This respects `suppressAggFilteredOnly` and `groupAggFiltering`; when those cause aggregation to include all children rather than just filtered ones, `aggregatedChildren` follows suit.
- **Pivot columns on leaf groups**: only the children matching the column's pivot keys, since pivot aggregation groups rows by pivot key values.
- **Non-group rows**: an empty array.

The same children can be retrieved programmatically with `rowNode.getAggregatedChildren(colKey)`. To collect all descendant leaf rows instead of only the immediate children, pass `true` as the second argument: `rowNode.getAggregatedChildren(colKey, true)`.

See [Retrieving Aggregated Children](https://www.ag-grid.com/javascript-data-grid/aggregation/#retrieving-aggregated-children) for more details.

### Cascading Edits

Key points for implementing cascading edits:

- Call `rowNode.setDataValue` on each child to write the new value down.
- When a child is itself a group with its own `groupRowValueSetter`, the cascade recurses further.
- Parent aggregates refresh automatically: child `data` changes re-run the column `aggFunc`, so editing a group total rebalances the children to match it.
- Aggregation is batched. All `setDataValue` calls within a single `groupRowValueSetter` invocation are processed before re-aggregation runs.

> **Note**
>
> The `aggregatedChildren` parameter and `rowNode.getAggregatedChildren()` method are only supported with the Client-Side Row Model. For other row models, `aggregatedChildren` is an empty array.

### Calling distributeGroupValue

The built-in distribution function `distributeGroupValue` is exported from `ag-grid-enterprise` and can be called directly inside a custom `groupRowValueSetter` callback. This is useful when custom logic is needed before or after the distribution:

```ts
import { distributeGroupValue } from 'ag-grid-enterprise';

colDef.groupRowValueSetter = (params) => {
    // Custom pre-processing
    const adjusted = Math.max(0, Number(params.newValue));
    return distributeGroupValue({ ...params, newValue: adjusted }, { distribution: 'percentage' });
};
```

You can also assign `distributeGroupValue` directly without a wrapper for default behaviour:

```ts
import { distributeGroupValue } from 'ag-grid-enterprise';

colDef.groupRowValueSetter = distributeGroupValue;
```

## Editing Pivot Columns

When using [Pivoting](https://www.ag-grid.com/javascript-data-grid/pivoting/), the `groupRowValueSetter` also works with pivot result columns. The key difference is that `aggregatedChildren` returns only the children matching the column's pivot keys, not all children of the group.

The following example uses the built-in distribution in pivot mode. Setting `groupRowEditable` enables distribution automatically. Double-click any pivot cell to edit it; the edited value is distributed uniformly among the children matching that pivot column's keys.

#### Built-in Pivot Editing

```ts
import {
  ClientSideRowModelModule,
  GridApi,
  GridOptions,
  ModuleRegistry,
  NumberEditorModule,
  NumberFilterModule,
  TextFilterModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  PivotModule,
  RowGroupingEditModule,
  RowGroupingModule,
} from "ag-grid-enterprise";
import { SalesRecord, getData } from "./data";

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

ModuleRegistry.registerModules([
  RowGroupingModule,
  RowGroupingEditModule,
  ClientSideRowModelModule,
  NumberEditorModule,
  NumberFilterModule,
  TextFilterModule,
  PivotModule,
]);

let gridApi: GridApi<SalesRecord>;

const gridOptions: GridOptions<SalesRecord> = {
  columnDefs: [
    { field: "region", rowGroup: true, hide: true },
    { field: "country", rowGroup: true, hide: true },
    { field: "product", pivot: true },
    {
      headerName: "Amount",
      field: "amount",
      aggFunc: "sum",
      editable: true,

      // Enable editing on group rows. When groupRowEditable is defined, the
      // built-in distribution is used automatically — 'uniform' for 'sum',
      // dividing the new total equally among children matching the pivot keys.
      groupRowEditable: true,
    },
  ],
  autoGroupColumnDef: {
    minWidth: 200,
    cellRendererParams: { suppressCount: true },
  },
  defaultColDef: {
    flex: 1,
    minWidth: 120,
    sortable: true,
    filter: true,
    resizable: true,
  },
  pivotMode: true,
  rowData: getData(),
  groupDefaultExpanded: -1,
  getRowId: ({ data }) => data.id,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
```

[Live example: Built-in Pivot Editing](https://www.ag-grid.com/examples/grouping-edit/pivot-editable-builtin/typescript)

For full control, use a custom `groupRowValueSetter` callback. In the example below:

- The grid is pivoted by `product` (Electronics, Clothing, Food), grouped by `region` and `country`.
- Double-clicking any pivot column cell opens the editor for the aggregated value.
- When editing a pivot cell (e.g. "Electronics" on the "Europe" row), `aggregatedChildren` contains only the European rows selling Electronics, not all European rows.
- The edit is distributed equally among those matching children, leaving other product categories unchanged.

#### Custom Editable Pivot Totals

```ts
import {
  ClientSideRowModelModule,
  GridApi,
  GridOptions,
  GroupRowValueSetterFunc,
  ModuleRegistry,
  NumberFilterModule,
  TextEditorModule,
  ValueParserParams,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnsToolPanelModule,
  PivotModule,
  RowGroupingEditModule,
  RowGroupingModule,
  SideBarModule,
} from "ag-grid-enterprise";
import { getData } from "./data";

interface SalesRecord {
  id: string;
  region: string;
  country: string;
  product: string;
  amount: number;
}

let gridApi: GridApi<SalesRecord>;

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

ModuleRegistry.registerModules([
  RowGroupingModule,
  RowGroupingEditModule,
  ClientSideRowModelModule,
  NumberFilterModule,
  TextEditorModule,
  PivotModule,
  SideBarModule,
  ColumnsToolPanelModule,
]);

// Parse input to integer
const amountValueParser = (params: ValueParserParams): number | null => {
  const numericValue = Number(params.newValue);
  return Number.isFinite(numericValue)
    ? Math.round(numericValue)
    : (params.oldValue ?? null);
};

/**
 * Distributes a new pivot total equally among children.
 *
 * In pivot mode, `aggregatedChildren` contains only rows matching the pivot keys.
 * For example, editing the "Electronics 2024" cell returns only rows where
 * product="Electronics" AND year=2024.
 *
 * `setDataValue` on leaf rows with pivot columns auto-resolves to the underlying
 * value column. On group rows, it triggers `groupRowValueSetter` for recursive cascade.
 */
const cascadeGroupTotal: GroupRowValueSetterFunc<SalesRecord> = ({
  column,
  newValue,
  eventSource,
  aggregatedChildren,
}) => {
  const total = Number(newValue);
  if (!Number.isFinite(total) || !aggregatedChildren.length) {
    return false;
  }

  // Distribute equally among children
  // https://en.wikipedia.org/wiki/Largest_remainder_method
  const count = aggregatedChildren.length;
  const base = Math.floor(total / count);
  let remainder = Math.round(total) - base * count;

  // Apply the distributed values
  let changed = false;
  for (const child of aggregatedChildren) {
    let value = base;
    if (remainder > 0) {
      value++;
      remainder--;
    }
    if (child.setDataValue(column, value, eventSource)) {
      changed = true;
    }
  }
  return changed;
};

const gridOptions: GridOptions<SalesRecord> = {
  columnDefs: [
    { field: "region", rowGroup: true, hide: true },
    { field: "country", rowGroup: true, hide: true },
    { field: "product", pivot: true },
    {
      headerName: "Amount",
      field: "amount",
      aggFunc: "sum",
      editable: true,
      groupRowEditable: true,
      valueParser: amountValueParser,
      groupRowValueSetter: cascadeGroupTotal,
    },
  ],
  autoGroupColumnDef: {
    minWidth: 200,
    cellRendererParams: {
      suppressCount: true,
    },
  },
  defaultColDef: {
    flex: 1,
    minWidth: 120,
    sortable: true,
    filter: true,
    resizable: true,
  },
  pivotMode: true,
  sideBar: "columns",
  rowData: getData(),
  groupDefaultExpanded: -1,
  getRowId: ({ data }) => data.id,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
```

[Live example: Custom Editable Pivot Totals](https://www.ag-grid.com/examples/grouping-edit/pivot-editable-totals-custom/typescript)

Edits on pivot columns therefore affect only the rows contributing to that pivot value, leaving other pivot categories untouched.

## Tree Data

`groupRowEditable` and `groupRowValueSetter` also work with [Tree Data](https://www.ag-grid.com/javascript-data-grid/tree-data/). Parent nodes act as group rows, so editing a parent's aggregated value cascades the change down to its children just as with row grouping.

The example below shows a company's budget organised as a tree: departments contain teams, and teams contain employees. Editing a department's or team's budget distributes the new total equally among its members, rounded to integers, recursing through the hierarchy.

#### Tree Data Editable

```ts
import {
  ClientSideRowModelModule,
  GridApi,
  GridOptions,
  ModuleRegistry,
  NumberEditorModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import { RowGroupingEditModule, TreeDataModule } from "ag-grid-enterprise";
import { BudgetRecord, getData } from "./data";

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

ModuleRegistry.registerModules([
  ClientSideRowModelModule,
  TreeDataModule,
  RowGroupingEditModule,
  NumberEditorModule,
]);

let gridApi: GridApi<BudgetRecord>;

const gridOptions: GridOptions<BudgetRecord> = {
  columnDefs: [
    {
      headerName: "Budget",
      field: "budget",
      aggFunc: "sum",
      editable: true,

      // Enable editing on group (department/team) rows.
      // The built-in distribution divides the new budget equally among
      // children, cascading through the full tree hierarchy.
      groupRowEditable: true,
      groupRowValueSetter: { precision: 0 },
    },
  ],
  defaultColDef: {
    flex: 1,
  },
  autoGroupColumnDef: {
    headerName: "Department / Team / Employee",
    field: "name",
    minWidth: 280,
    cellRendererParams: { suppressCount: true },
  },
  treeData: true,
  treeDataChildrenField: "children",
  groupDefaultExpanded: -1,
  getRowId: ({ data }) => data.name,
  rowData: getData(),
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
```

[Live example: Tree Data Editable](https://www.ag-grid.com/examples/grouping-edit/tree-data-editable/typescript)

## Refreshing Groups After Editing

When grouped columns are editable, setting `refreshAfterGroupEdit=true` causes the grid to update row data and recalculate the grouping after every committed edit. Without this option, the row data updates but the grouping does not get updated until the next full refresh.

When enabling `refreshAfterGroupEdit`, also provide `getRowId` so that the grid can track rows by stable IDs while rebuilding the grouping hierarchy.

The following example demonstrates this behaviour. Double-click on a `department` or `team` cell to edit it; the grid re-evaluates the grouping and moves the row to the correct group instantly.

#### Refresh After Group Edit

```ts
import {
  ClientSideRowModelModule,
  GridOptions,
  ModuleRegistry,
  SelectEditorModule,
  TextEditorModule,
  TextFilterModule,
  createGrid,
  enableDevValidations,
} from "ag-grid-community";
import { RowGroupingModule } from "ag-grid-enterprise";

interface Employee {
  id: string;
  department: string;
  team: string;
  name: string;
  role: string;
}

const departments = ["Engineering", "Sales", "Marketing"];
const teams: Record<string, string[]> = {
  Engineering: ["Frontend", "Backend", "QA"],
  Sales: ["Enterprise", "SMB", "Partners"],
  Marketing: ["Content", "Growth", "Brand"],
};
const allTeams = Object.values(teams).flat();

function getData(): Employee[] {
  return [
    // Engineering
    {
      id: "1",
      department: "Engineering",
      team: "Frontend",
      name: "Alice",
      role: "Developer",
    },
    {
      id: "2",
      department: "Engineering",
      team: "Frontend",
      name: "Bob",
      role: "Developer",
    },
    {
      id: "3",
      department: "Engineering",
      team: "Backend",
      name: "Carol",
      role: "Developer",
    },
    {
      id: "4",
      department: "Engineering",
      team: "Backend",
      name: "Dave",
      role: "Tech Lead",
    },
    {
      id: "5",
      department: "Engineering",
      team: "QA",
      name: "Eve",
      role: "Tester",
    },

    // Sales
    {
      id: "6",
      department: "Sales",
      team: "Enterprise",
      name: "Frank",
      role: "Account Exec",
    },
    {
      id: "7",
      department: "Sales",
      team: "Enterprise",
      name: "Grace",
      role: "Account Exec",
    },
    {
      id: "8",
      department: "Sales",
      team: "SMB",
      name: "Hank",
      role: "Sales Rep",
    },
    {
      id: "9",
      department: "Sales",
      team: "Partners",
      name: "Ivy",
      role: "Partner Manager",
    },

    // Marketing
    {
      id: "10",
      department: "Marketing",
      team: "Content",
      name: "Jack",
      role: "Writer",
    },
    {
      id: "11",
      department: "Marketing",
      team: "Growth",
      name: "Kim",
      role: "Analyst",
    },
    {
      id: "12",
      department: "Marketing",
      team: "Brand",
      name: "Leo",
      role: "Designer",
    },
  ];
}

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

ModuleRegistry.registerModules([
  RowGroupingModule,
  ClientSideRowModelModule,
  TextEditorModule,
  TextFilterModule,
  SelectEditorModule,
]);

const gridOptions: GridOptions<Employee> = {
  columnDefs: [
    {
      field: "department",
      rowGroup: true,
      editable: true,
      cellEditor: "agSelectCellEditor",
      cellEditorParams: { values: departments },
    },
    {
      field: "team",
      rowGroup: true,
      editable: true,
      cellEditor: "agSelectCellEditor",
      cellEditorParams: { values: allTeams },
    },
    { field: "name" },
    { field: "role", editable: true },
  ],
  defaultColDef: {
    flex: 1,
    sortable: true,
    resizable: true,
    filter: true,
  },
  autoGroupColumnDef: {
    minWidth: 250,
  },
  rowData: getData(),
  refreshAfterGroupEdit: true,
  groupDefaultExpanded: -1,
  animateRows: true,
  getRowId: ({ data }) => data.id,
};

const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
createGrid(gridDiv, gridOptions);
```

[Live example: Refresh After Group Edit](https://www.ag-grid.com/examples/grouping-edit/refresh-after-group-edit/typescript)

See also [Read Only Edit](https://www.ag-grid.com/javascript-data-grid/value-setters/#read-only-edit) for configuring immutable grouped data or connecting the grid with a store.
