diff --git a/community-modules/styles/src/internal/base/_base-variables.scss b/community-modules/styles/src/internal/base/_base-variables.scss
index 7a49bdffb96..51b8a8069ce 100644
--- a/community-modules/styles/src/internal/base/_base-variables.scss
+++ b/community-modules/styles/src/internal/base/_base-variables.scss
@@ -242,6 +242,9 @@
 
         --ag-filter-panel-apply-button-color: var(--ag-foreground-color);
         --ag-filter-panel-apply-button-background-color: var(--ag-background-color);
+
+        --ag-column-panel-apply-button-color: var(--ag-foreground-color);
+        --ag-column-panel-apply-button-background-color: var(--ag-background-color);
         --ag-filter-panel-card-subtle-color: var(--ag-foreground-color);
         --ag-filter-panel-card-subtle-hover-color: var(--ag-foreground-color);
 
diff --git a/community-modules/styles/src/internal/base/parts/_columns-tool-panel.scss b/community-modules/styles/src/internal/base/parts/_columns-tool-panel.scss
index c5c525fbac0..d538e8a16ce 100644
--- a/community-modules/styles/src/internal/base/parts/_columns-tool-panel.scss
+++ b/community-modules/styles/src/internal/base/parts/_columns-tool-panel.scss
@@ -1,14 +1,6 @@
 @use 'ag';
 
 @mixin output {
-    :where(.ag-column-panel.ag-column-panel-deferred),
-    :where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-panel-column-select),
-    :where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-drop-vertical),
-    :where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-panel-defer-mode-toggle),
-    :where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-panel-buttons) {
-        background-color: var(--ag-cell-batch-edit-background-color);
-    }
-
     .ag-pivot-mode-panel {
         min-height: var(--ag-header-height);
         height: var(--ag-header-height);
@@ -46,31 +38,32 @@
 
     .ag-column-panel-buttons {
         display: flex;
-        flex-wrap: wrap;
-        gap: var(--ag-widget-vertical-spacing) var(--ag-widget-horizontal-spacing);
         justify-content: flex-end;
         overflow: hidden;
-        padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding);
-        border-top: var(--ag-borders-secondary) var(--ag-secondary-border-color);
-    }
+        padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding) 0;
+        flex-wrap: wrap;
+        gap: var(--ag-widget-vertical-spacing) var(--ag-widget-horizontal-spacing);
 
-    .ag-column-panel-defer-mode-toggle {
-        display: flex;
-        align-items: center;
-        gap: var(--ag-widget-horizontal-spacing);
-        padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding);
-        border-top: var(--ag-borders-secondary) var(--ag-secondary-border-color);
+        .ag-standard-button {
+            transition:
+                background-color 0.25s ease-in-out,
+                color 0.25s ease-in-out;
+        }
     }
 
-    .ag-column-panel-defer-mode-select {
-        display: flex;
-        align-items: center;
+    .ag-column-panel-buttons:last-child {
+        padding-bottom: var(--ag-widget-container-vertical-padding);
     }
 
     .ag-column-panel-buttons-button {
         line-height: 1.5;
     }
 
+    .ag-column-panel .ag-standard-button.ag-column-panel-buttons-apply-button {
+        color: var(--ag-column-panel-apply-button-color);
+        background-color: var(--ag-column-panel-apply-button-background-color);
+    }
+
     .ag-column-group-icons,
     .ag-column-select-header-icon {
         color: var(--ag-secondary-foreground-color);
diff --git a/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss b/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss
index da4ab07bc76..23f925fa61b 100644
--- a/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss
+++ b/community-modules/styles/src/internal/themes/alpine/_alpine-variables.scss
@@ -51,6 +51,9 @@
     --ag-menu-background-color: var(--ag-header-background-color);
     --ag-filter-panel-apply-button-color: var(--ag-background-color);
     --ag-filter-panel-apply-button-background-color: var(--ag-alpine-active-color);
+
+    --ag-column-panel-apply-button-color: var(--ag-background-color);
+    --ag-column-panel-apply-button-background-color: var(--ag-alpine-active-color);
     --ag-column-drag-indicator-color: var(--ag-alpine-active-color);
 
     // derived and blended colours (these are static versions of the dynamic colour blends
@@ -133,6 +136,7 @@
     --ag-find-match-color: var(--ag-background-color);
     --ag-find-active-match-color: var(--ag-background-color);
     --ag-filter-panel-apply-button-color: var(--ag-foreground-color);
+    --ag-column-panel-apply-button-color: var(--ag-foreground-color);
 
     --ag-row-loading-skeleton-effect-color: #{color.change(#cacbcc, $alpha: 0.4)};
 
diff --git a/community-modules/styles/src/internal/themes/balham/_balham-variables.scss b/community-modules/styles/src/internal/themes/balham/_balham-variables.scss
index 0249a1c950e..bb4dade4d3a 100644
--- a/community-modules/styles/src/internal/themes/balham/_balham-variables.scss
+++ b/community-modules/styles/src/internal/themes/balham/_balham-variables.scss
@@ -38,6 +38,9 @@
     --ag-panel-background-color: var(--ag-header-background-color);
     --ag-filter-panel-apply-button-color: var(--ag-background-color);
     --ag-filter-panel-apply-button-background-color: var(--ag-balham-active-color);
+
+    --ag-column-panel-apply-button-color: var(--ag-background-color);
+    --ag-column-panel-apply-button-background-color: var(--ag-balham-active-color);
     --ag-column-drag-indicator-color: var(--ag-balham-active-color);
 
     // derived and blended colours (these are static versions of the dynamic colour blends
@@ -106,6 +109,7 @@
     --ag-find-match-color: var(--ag-background-color);
     --ag-find-active-match-color: var(--ag-background-color);
     --ag-filter-panel-apply-button-color: var(--ag-foreground-color);
+    --ag-column-panel-apply-button-color: var(--ag-foreground-color);
 
     // override blended colors from balham theme
     --ag-secondary-foreground-color: var(--ag-foreground-color);
diff --git a/community-modules/styles/src/internal/themes/material/_material-variables.scss b/community-modules/styles/src/internal/themes/material/_material-variables.scss
index 0f2d5ef6af6..df95850b8cc 100644
--- a/community-modules/styles/src/internal/themes/material/_material-variables.scss
+++ b/community-modules/styles/src/internal/themes/material/_material-variables.scss
@@ -42,6 +42,9 @@
     --ag-filter-panel-apply-button-color: var(--ag-material-primary-color);
     --ag-filter-panel-apply-button-background-color: transparent;
 
+    --ag-column-panel-apply-button-color: var(--ag-material-primary-color);
+    --ag-column-panel-apply-button-background-color: transparent;
+
     // derived colours (no color blending)
     --ag-range-selection-border-color: var(--ag-material-primary-color);
     --ag-checkbox-checked-color: var(--ag-material-accent-color);
diff --git a/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss b/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss
index 1923d2c7493..8e3662c87b7 100644
--- a/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss
+++ b/community-modules/styles/src/internal/themes/quartz/_quartz-variables.scss
@@ -113,6 +113,9 @@
 
     --ag-filter-panel-apply-button-color: var(--ag-background-color);
     --ag-filter-panel-apply-button-background-color: var(--ag-active-color);
+
+    --ag-column-panel-apply-button-color: var(--ag-background-color);
+    --ag-column-panel-apply-button-background-color: var(--ag-active-color);
 }
 
 @mixin -dark-vars {
@@ -143,6 +146,7 @@
     --ag-find-match-color: var(--ag-background-color);
     --ag-find-active-match-color: var(--ag-background-color);
     --ag-filter-panel-apply-button-color: var(--ag-foreground-color);
+    --ag-column-panel-apply-button-color: var(--ag-foreground-color);
 
     --ag-popup-shadow: 0 0px 20px rgba(0, 0, 0, 0.3);
 
diff --git a/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json b/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
index cc7fcc31d45..50d52160310 100644
--- a/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
+++ b/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
@@ -3,13 +3,12 @@
         "codeSrc": "grid-api.AUTO.json",
         "validate": true,
         "undocumentedProperties": {
-            "sortChanged": "TODO: undocumented property, to resolve in AG-16879",
-            "flushAllAnimationFrames": "TODO: undocumented property, to resolve in AG-16879",
-            "getColumnDef": "TODO: undocumented property, to resolve in AG-16879",
-            "filterChanged": "TODO: undocumented property, to resolve in AG-16879",
-            "getInfiniteRowCount": "TODO: undocumented property, to resolve in AG-16879",
-            "flushServerSideAsyncTransactions": "TODO: undocumented property, to resolve in AG-16879",
-            "dispatchEvent": "TODO: undocumented property, to resolve in AG-16879"
+            "flushAllAnimationFrames": "Not recommended for users so not documented",
+            "flushServerSideAsyncTransactions": "Not recommended for users to not documented",
+            "dispatchEvent": "This is for internal use, purposefully undocumented",
+            "sortChanged": "Bug in the undocumentedProperties check, this is not an api method",
+            "filterChanged": "Bug in the undocumentedProperties check, this is not an api method",
+            "getColumnDef": "Duplicate, users can just use getColumnDefs."
         }
     },
     "gridOptions": {
diff --git a/documentation/ag-grid-docs/src/content/api-documentation/grid-events/events.json b/documentation/ag-grid-docs/src/content/api-documentation/grid-events/events.json
index 0177f6db7ab..e458be2b001 100644
--- a/documentation/ag-grid-docs/src/content/api-documentation/grid-events/events.json
+++ b/documentation/ag-grid-docs/src/content/api-documentation/grid-events/events.json
@@ -3,13 +3,7 @@
         "codeSrc": "grid-options.AUTO.json",
         "validate": true,
         "onlyEvents": true,
-        "undocumentedProperties": {
-            "bulkEditingStarted": "TODO: undocumented property, to resolve in AG-16879",
-            "bulkEditingStopped": "TODO: undocumented property, to resolve in AG-16879",
-            "dragCancelled": "TODO: undocumented property, to resolve in AG-16879",
-            "rowResizeStarted": "TODO: undocumented property, to resolve in AG-16879",
-            "rowResizeEnded": "TODO: undocumented property, to resolve in AG-16879"
-        }
+        "undocumentedProperties": {}
     },
     "accessories": {
         "meta": {
@@ -227,6 +221,18 @@
                 "url": "./cell-editing-batch/#batch-editing-lifecycle"
             }
         },
+        "bulkEditingStarted": {
+            "more": {
+                "name": "Bulk Cell Edit",
+                "url": "./cell-selection/#bulk-cell-edit"
+            }
+        },
+        "bulkEditingStopped": {
+            "more": {
+                "name": "Bulk Cell Edit",
+                "url": "./cell-selection/#bulk-cell-edit"
+            }
+        },
         "undoStarted": {
             "more": {
                 "name": "Undo / Redo Events",
@@ -393,9 +399,30 @@
         "rowGroupOpened": {},
         "expandOrCollapseAll": {}
     },
-    "miscellaneous": {
+    "rowEvents": {
         "meta": {
-            "displayName": "Miscellaneous"
+            "displayName": "Row Size",
+            "page": {
+                "name": "Row Resizing",
+                "url": "./row-numbers/#row-resizing"
+            }
+        },
+        "rowResizeStarted": {
+            "more": {
+                "name": "Row Resizing",
+                "url": "./row-numbers/#row-resizing"
+            }
+        },
+        "rowResizeEnded": {
+            "more": {
+                "name": "Row Resizing",
+                "url": "./row-numbers/#row-resizing"
+            }
+        }
+    },
+    "gridLifecycle": {
+        "meta": {
+            "displayName": "Grid Lifecycle"
         },
         "gridReady": {
             "more": {
@@ -415,25 +442,36 @@
                 "url": "./grid-lifecycle/#first-data-rendered"
             }
         },
+        "modelUpdated": {},
+        "stateUpdated": {
+            "more": {
+                "name": "Grid State",
+                "url": "./grid-state/"
+            }
+        }
+    },
+    "viewport": {
+        "meta": {
+            "displayName": "Viewport"
+        },
         "gridSizeChanged": {
             "more": {
                 "name": "Grid Layout",
                 "url": "./grid-size/"
             }
         },
-        "modelUpdated": {},
         "virtualRowRemoved": {},
         "viewportChanged": {},
         "bodyScroll": {},
-        "bodyScrollEnd": {},
+        "bodyScrollEnd": {}
+    },
+    "dragAndDrop": {
+        "meta": {
+            "displayName": "Drag and Drop"
+        },
         "dragStarted": {},
         "dragStopped": {},
-        "stateUpdated": {
-            "more": {
-                "name": "Grid State",
-                "url": "./grid-state/"
-            }
-        }
+        "dragCancelled": {}
     },
     "find": {
         "meta": {
diff --git a/documentation/ag-grid-docs/src/content/docs/cell-data-types/index.mdoc b/documentation/ag-grid-docs/src/content/docs/cell-data-types/index.mdoc
index e205d60403b..d1449ced402 100644
--- a/documentation/ag-grid-docs/src/content/docs/cell-data-types/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/cell-data-types/index.mdoc
@@ -42,20 +42,32 @@ The following example demonstrates the pre-defined cell data types (most of whic
 
 ## Inferring Data Types
 
-By default, the grid will infer cell data types the first time that row data is passed into the grid.
+By default, when using the Client-Side Row Model, the grid will infer cell data types the first time that row data is passed into the grid given the following conditions:
 
-For inference to work for a column, it must contain non-null values and have the `field` property set. The resolved column definition (including the default column definition and column types) must also not have the Value Getter, Value Parser or reference data properties set, or be using [Sparklines](./sparklines-overview/). If these conditions are not met, no cell data type will be set (it will need to be defined directly on the column if desired).
+**The column must:**
 
-{% note %}
-Because `'dateTime'` corresponds to cell values that are `Date` objects, there is no easy way to tell them apart from regular `'date'` columns. If you wish to enable `'dateTime'`'s higher precision fields, please explicitly specify `cellDataType: 'dateTime'` in the corresponding `columnDefs` entry.
-{% /note %}
+-   Have a `field` property set
+-   Contain at least one non-null value
+
+**The column must not:**
+
+-   Have a `valueGetter`, `valueParser`, or `refData` property set on the resolved column definition (including the default column definition and column types)
+-   Use [Sparklines](./sparklines-overview/)
+
+If any condition is not met, no cell data type will be inferred. You can still set one explicitly via `cellDataType` on the column definition.
+
+Where inference is possible but the values do not match any pre-defined type, it defaults to `'object'`.
+
+### Disabling Inference
+
+Set `cellDataType: false` on an individual column, or on the [Default Column Definition](./column-definitions/#default-column-definitions) to disable it globally.
 
-Data type inference can be disabled by setting `cellDataType = false` on an individual column, or for all columns on the [Default Column Definition](./column-definitions/#default-column-definitions).
+### Date vs DateTime
 
-Note that where inference is possible, but it does not match any of the pre-defined cell data types, it will default to `object`.
+Because `'dateTime'` values are `Date` objects — the same as `'date'` — the grid cannot distinguish between them during inference. To use the higher-precision `'dateTime'` type, set `cellDataType: 'dateTime'` explicitly on the column definition.
 
 {% note %}
-Inferring cell data types only works for the Client-Side Row Model. For other row models, you will need to define cell data types for each column.
+Inference only works with the Client-Side Row Model. For other row models, define `cellDataType` explicitly on each column.
 {% /note %}
 
 ## Pre-Defined Cell Data Types
@@ -192,7 +204,7 @@ The following properties are set:
 -   `cellEditorParams.useFormatter = true` so that the cell editor uses the Value Formatter.
 -   A `comparator` is defined to allow [Custom Sorting](./row-sorting/#custom-sorting) using the Value Formatter.
 -   When the [Text Filter](./filter-text/) is used, a [Filter Value Getter](./column-properties/#reference-filtering-filterValueGetter) is used to convert the value with the Value Formatter.
--   When the [Set Filter](./filter-set) is used, `filterParams.valueFormatter` is set to format the values using the Value Formatter.
+-   When the [Set Filter](./filter-set) is used with [Complex Objects](./filter-set-filter-list/#complex-objects), `filterParams.valueFormatter` is set to format the values using the Value Formatter.
 
 ### Pre-Defined Cell Data Type Example
 
diff --git a/documentation/ag-grid-docs/src/content/docs/filter-set-filter-list/index.mdoc b/documentation/ag-grid-docs/src/content/docs/filter-set-filter-list/index.mdoc
index 2413687e945..85bc96de8cb 100644
--- a/documentation/ag-grid-docs/src/content/docs/filter-set-filter-list/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/filter-set-filter-list/index.mdoc
@@ -83,7 +83,7 @@ In the code above, the same value formatter is supplied to the Column and Filter
 
 There is not a one-to-one mapping between Filter List items and rows (as values from multiple rows may map to the same Filter List item), so the Value Formatter for the Filter List will not be passed `node` or `data` params, and should just format based on the `value`.
 
-If the data contains [Missing Values](#missing-values), the Value Formatter will need to handle how to display the blank values (e.g. return `'(Blanks)'`).
+If the data contains [Missing Values](#missing-values), the Value Formatter will need to handle how to display the blank values (e.g. return `'(Blanks)'`). If the data contains complex objects, see [Complex Objects](#complex-objects) for how to use the Value Formatter alongside a Key Creator.
 
 The following example shows how Set Filter values are formatted using a Value Formatter. Note the following:
 
@@ -310,7 +310,7 @@ If there are missing / empty values in the row data of the grid, or missing valu
 
 The Set Filter internally maintains the original type of the cell values, but always uses strings for the keys. E.g. if the cell contains a number, the Filter Model will contain those numbers converted to strings, but if you specified a value formatter, that would use the values with type number.
 
-### Complex Objects
+## Complex Objects
 
 If you are providing complex objects as values, then you need to provide both a Key Creator function and a Value Formatter function when using the Set Filter.
 
@@ -318,6 +318,10 @@ The Key Creator generates a unique string key from the complex object (note that
 
 The Value Formatter is used to generate the label that is displayed to the user within the Filter List. You can provide the Value Formatter in the filter params.
 
+{% note %}
+See [Cell Data Types - Object](/javascript-data-grid/cell-data-types/#object) for more information on working with complex objects in AG Grid.
+{% /note %}
+
 ```{% frameworkTransform=true spaceBetweenProperties=true %}
 const gridOptions = {
     columnDefs: [
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-lifecycle/index.mdoc b/documentation/ag-grid-docs/src/content/docs/grid-lifecycle/index.mdoc
index 01204181189..ea8cbc7e24c 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-lifecycle/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grid-lifecycle/index.mdoc
@@ -31,7 +31,7 @@ The `firstDataRendered` event fires the first time data is rendered into the gri
 
 The `rowDataUpdated` event fires every time the grid's data changes, by [Updating Row Data](./data-update-row-data/) or
 by applying [Transaction Updates](./data-update-transactions/). In the [Server Side Row Model](./server-side-model), use
-the [Model Updated Event](./grid-events/#reference-miscellaneous-modelUpdated) instead.
+the [Model Updated Event](./grid-events/#reference-gridLifecycle-modelUpdated) instead.
 
 In this example the time at which `firstDataRendered` and `rowDataUpdated` are fired is recorded above the grid. Note that `firstDataRendered` is only set on the initial load of the grid and is not updated when reloading data.
 
diff --git a/documentation/ag-grid-docs/src/content/docs/grid-state/index.mdoc b/documentation/ag-grid-docs/src/content/docs/grid-state/index.mdoc
index a2ded4a76c6..8c6fdf87366 100644
--- a/documentation/ag-grid-docs/src/content/docs/grid-state/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grid-state/index.mdoc
@@ -37,7 +37,7 @@ The current grid state can be retrieved by listening to the state updated event,
 
 The state is also passed in the [Grid Pre-Destroyed Event](./grid-lifecycle/#grid-pre-destroyed), which can be used to get the state when the grid is destroyed.
 
-{% apiDocumentation source="grid-events/events.json" section="miscellaneous" names=["stateUpdated", "gridPreDestroyed"] /%}
+{% apiDocumentation source="grid-events/events.json" section="gridLifecycle" names=["stateUpdated", "gridPreDestroyed"] /%}
 
 ## State Contents
 
diff --git a/documentation/ag-grid-docs/src/content/docs/row-dragging-unmanaged/index.mdoc b/documentation/ag-grid-docs/src/content/docs/row-dragging-unmanaged/index.mdoc
index 8ec617a1cc5..87fbe7c324f 100644
--- a/documentation/ag-grid-docs/src/content/docs/row-dragging-unmanaged/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/row-dragging-unmanaged/index.mdoc
@@ -46,7 +46,7 @@ Typically a drag will fire the following events:
 Additional `rowDragLeave` and `rowDragEnter` events are fired if the mouse leaves or re-enters the grid. If the drag is finished outside of the grid, then the `rowDragLeave` is the last event fired and no `rowDragEnd` is fired, as the drag did not end on the grid.
 
 {% note %}
-When the Grid is created, a [Drop Zone](./row-dragging-to-external-dropzone/) that is responsible for firing all the Row Drag Events is added to the Grid Body. This why Row Drag Events (including `rowDragEnd`) are only fired when they happen on top of the Grid. If you need to monitor when a Row Drag ends outside of the Grid, for example, use the [DragStopped](./grid-events/#reference-miscellaneous-dragStopped) event.
+When the Grid is created, a [Drop Zone](./row-dragging-to-external-dropzone/) that is responsible for firing all the Row Drag Events is added to the Grid Body. This why Row Drag Events (including `rowDragEnd`) are only fired when they happen on top of the Grid. If you need to monitor when a Row Drag ends outside of the Grid, for example, use the [DragStopped](./grid-events/#reference-dragAndDrop-dragStopped) event.
 {% /note %}
 
 Each of the four row drag events extend the `RowDragEvent` interface.
diff --git a/documentation/ag-grid-docs/src/content/docs/row-numbers/index.mdoc b/documentation/ag-grid-docs/src/content/docs/row-numbers/index.mdoc
index 73ed5e8d920..bfe2d7de2e5 100644
--- a/documentation/ag-grid-docs/src/content/docs/row-numbers/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/row-numbers/index.mdoc
@@ -68,6 +68,12 @@ const gridOptions = {
 The Row Resizer feature does not work when columns are configured with [Auto Row Height](./row-height/#auto-row-height).
 {% /note %}
 
+### Row Resize Events
+
+The following events are fired when a row resize operation starts and ends.
+
+{% apiDocumentation source="grid-events/events.json" section="rowEvents" names=["rowResizeStarted", "rowResizeEnded"] /%}
+
 ## Value Export
 
 By default, when working with exporters such as [CSV Export](./csv-export/) or [Excel Export](./excel-export/), the value of the Row Numbers column is not exported.
diff --git a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode/main.ts b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode/main.ts
index 8856db31fe5..53d4cf3162c 100644
--- a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/_examples/deferred-apply-mode/main.ts
@@ -88,7 +88,7 @@ const gridOptions: GridOptions<IOlympicData> = {
                 iconKey: 'columns',
                 toolPanel: 'agColumnsToolPanel',
                 toolPanelParams: {
-                    deferApply: true,
+                    buttons: ['cancel', 'apply'],
                 },
             },
         ],
diff --git a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc
index 6afabab7a03..3b4f260ef4b 100644
--- a/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/tool-panel-columns/index.mdoc
@@ -39,7 +39,7 @@ const gridOptions = {
                 iconKey: 'columns',
                 toolPanel: 'agColumnsToolPanel',
                 toolPanelParams: {
-                    deferApply: true,
+                    buttons: ['cancel', 'apply'],
                 },
             },
         ],
@@ -48,10 +48,10 @@ const gridOptions = {
 };
 ```
 
-When `deferApply=true`:
+When `buttons` includes `'apply'`:
 
 -   Changes made in the Columns Tool Panel are staged as pending changes.
--   The Columns Tool Panel shows **Apply** and **Cancel** actions.
+-   The configured buttons are displayed at the bottom of the tool panel.
 -   **Apply** commits pending changes.
 -   **Cancel** discards pending changes.
 
diff --git a/packages/ag-grid-community/src/api/gridApi.ts b/packages/ag-grid-community/src/api/gridApi.ts
index 90ad0dbddb3..bda03aa1dfc 100644
--- a/packages/ag-grid-community/src/api/gridApi.ts
+++ b/packages/ag-grid-community/src/api/gridApi.ts
@@ -1430,9 +1430,9 @@ export interface _InfiniteRowModelGridApi {
      */
     purgeInfiniteCache(): void;
 
-    /** The row count defines how many rows the grid allows scrolling to. */
-
-    /** @deprecated v32.2 gridApi.getInfiniteRowCount() deprecated, please use gridApi.getDisplayedRowCount() instead. */
+    /**
+     * @deprecated v32.2 gridApi.getInfiniteRowCount() deprecated, please use gridApi.getDisplayedRowCount() instead.
+     */
     getInfiniteRowCount(): number | undefined;
 }
 
diff --git a/packages/ag-grid-community/src/columns/columnFactoryUtils.ts b/packages/ag-grid-community/src/columns/columnFactoryUtils.ts
index 6a9f03b4dd3..4a374875a62 100644
--- a/packages/ag-grid-community/src/columns/columnFactoryUtils.ts
+++ b/packages/ag-grid-community/src/columns/columnFactoryUtils.ts
@@ -389,7 +389,7 @@ export function _addColumnDefaultAndTypes(
     }
 
     dataTypeSvc?.postProcess(res);
-    dataTypeSvc?.validateColDef(res);
+    dataTypeSvc?.validateColDef(res, colDef, defaultColDef, colId);
 
     gos.validateColDef(res, colId, isAutoCol);
 
diff --git a/packages/ag-grid-community/src/columns/dataTypeService.ts b/packages/ag-grid-community/src/columns/dataTypeService.ts
index 75573643d7e..72f605c9225 100644
--- a/packages/ag-grid-community/src/columns/dataTypeService.ts
+++ b/packages/ag-grid-community/src/columns/dataTypeService.ts
@@ -490,9 +490,13 @@ export class DataTypeService extends BeanStub implements NamedBean {
         return dataTypeMatcher(value);
     }
 
-    public validateColDef(colDef: ColDef): void {
-        const warning = (property: 'Formatter' | 'Parser') => _warn(48, { property });
+    public validateColDef(colDef: ColDef, userColDef?: ColDef, defaultColDef?: ColDef, colId?: string): void {
         if (colDef.cellDataType === 'object') {
+            const wasInferred = (colDef?: ColDef) => {
+                return colDef?.cellDataType == null || colDef?.cellDataType === true;
+            };
+            const inferred = wasInferred(userColDef) && wasInferred(defaultColDef);
+            const warning = (property: 'Formatter' | 'Parser') => _warn(48, { property, inferred, colId });
             const { object } = this.dataTypeDefinitions;
             if (colDef.valueFormatter === object.groupSafeValueFormatter && !this.hasObjectValueFormatter) {
                 warning('Formatter');
diff --git a/packages/ag-grid-community/src/interfaces/iColumnStateUpdateStrategy.ts b/packages/ag-grid-community/src/interfaces/iColumnStateUpdateStrategy.ts
index 5c8c05efdad..52c7a74b8d7 100644
--- a/packages/ag-grid-community/src/interfaces/iColumnStateUpdateStrategy.ts
+++ b/packages/ag-grid-community/src/interfaces/iColumnStateUpdateStrategy.ts
@@ -7,6 +7,7 @@ import type { ColumnEventType } from '../events';
 export interface IColumnStateUpdateStrategy {
     applyColumnState(deferMode: boolean, state: ColumnState[], eventType: ColumnEventType): void;
     commit(deferMode: boolean): void;
+    hasPendingChanges(deferMode: boolean): boolean;
     moveColumns(deferMode: boolean, columns: AgColumn[], targetIndex: number, eventType: ColumnEventType): void;
     reset(deferMode: boolean): void;
     setColumnsVisible(deferMode: boolean, columns: AgColumn[], visible: boolean, eventType: ColumnEventType): void;
@@ -14,6 +15,7 @@ export interface IColumnStateUpdateStrategy {
     setRowGroupColumns(deferMode: boolean, columns: AgColumn[], eventType: ColumnEventType): void;
     getRowGroupColumns(deferMode: boolean): AgColumn[];
     getPrimaryColumns(deferMode: boolean): AgColumn[];
+    hasDeferredColumnOrder(deferMode: boolean): boolean;
     setValueColumns(deferMode: boolean, columns: AgColumn[], eventType: ColumnEventType): void;
     getValueColumns(deferMode: boolean): AgColumn[];
     setColumnAggFunc(
diff --git a/packages/ag-grid-community/src/interfaces/iToolPanel.ts b/packages/ag-grid-community/src/interfaces/iToolPanel.ts
index 73502366593..2495c82dcfa 100644
--- a/packages/ag-grid-community/src/interfaces/iToolPanel.ts
+++ b/packages/ag-grid-community/src/interfaces/iToolPanel.ts
@@ -2,6 +2,8 @@ import type { IComponent } from '../agStack/interfaces/iComponent';
 import type { AgGridCommon } from './iCommon';
 import type { FilterAction } from './iFilter';
 
+export type ColumnToolPanelAction = 'apply' | 'cancel';
+
 export interface BaseToolPanelParams<TData = any, TContext = any, TState = any> extends AgGridCommon<TData, TContext> {
     /** The tool-panel-specific initial state as provided in grid options if applicable */
     initialState?: TState;
@@ -52,8 +54,8 @@ export interface IToolPanelColumnCompParams {
     contractColumnSelection: boolean;
     /** Suppress updating the layout of columns as they are rearranged in the grid */
     suppressSyncLayoutWithGrid: boolean;
-    /** Defer applying Columns Tool Panel changes until an explicit apply action */
-    deferApply?: boolean;
+    /** Buttons to display at the bottom of the Columns Tool Panel. When 'apply' is included, changes are deferred until the apply button is clicked. */
+    buttons?: ColumnToolPanelAction[];
 }
 
 export interface IToolPanelFiltersCompParams {
diff --git a/packages/ag-grid-community/src/main.ts b/packages/ag-grid-community/src/main.ts
index c90f389c8ee..e6b862059c7 100644
--- a/packages/ag-grid-community/src/main.ts
+++ b/packages/ag-grid-community/src/main.ts
@@ -419,6 +419,7 @@ export type {
 } from './interfaces/iNewFiltersToolPanel';
 export type {
     BaseToolPanelParams,
+    ColumnToolPanelAction,
     IToolPanel,
     IToolPanelColumnCompParams,
     IToolPanelComp,
diff --git a/packages/ag-grid-community/src/validation/errorMessages/errorText.ts b/packages/ag-grid-community/src/validation/errorMessages/errorText.ts
index fc3fd89f09f..0b181ec70e2 100644
--- a/packages/ag-grid-community/src/validation/errorMessages/errorText.ts
+++ b/packages/ag-grid-community/src/validation/errorMessages/errorText.ts
@@ -227,8 +227,12 @@ export const AG_GRID_ERRORS = {
         `The data type definition ${parentCellDataType} does not exist.` as const,
     46: () => 'The "baseDataType" property of a data type definition must match that of its parent.' as const,
     47: ({ cellDataType }: { cellDataType: string }) => `Missing data type definition - "${cellDataType}"` as const,
-    48: ({ property }: { property: string }) =>
-        `Cell data type is "object" but no Value ${property} has been provided. Please either provide an object data type definition with a Value ${property}, or set "colDef.value${property}"` as const,
+    48: ({ property, inferred, colId }: { property: string; inferred: boolean | undefined; colId?: string }) => {
+        const inferredStr = inferred ? ' (inferred)' : '';
+        const colIdStr = colId ? ` for column "${colId}"` : '';
+        const parserHint = inferred && property === 'Parser' ? '\n  - "colDef.cellDataType = \'object\'"' : '';
+        return `Cell data type is "object"${inferredStr} but no Value ${property} has been provided${colIdStr}. Please either provide an object data type definition with a Value ${property}, or set:\n  - "colDef.value${property}"${parserHint}` as const;
+    },
     49: ({ methodName }: { methodName: string }) =>
         `Framework component is missing the method ${methodName}()` as const,
     50: ({ compName }: { compName: string | undefined }) =>
@@ -729,6 +733,8 @@ export const AG_GRID_ERRORS = {
         'Since v35, `api.hideOverlay()` does not hide the overlay when `activeOverlay` is set. Set `activeOverlay=null` instead.' as const,
     297: () =>
         '`api.hideOverlay()` does not hide the no matching rows overlay as it is only controlled by grid state. Set `suppressOverlays=["noMatchingRows"] to not show it.' as const,
+    298: () =>
+        `Columns Tool Panel 'buttons' requires 'apply' to enable deferred mode. Without it, other buttons have no effect.` as const,
 };
 
 export type ErrorMap = typeof AG_GRID_ERRORS;
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts b/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts
index d8db4f05961..dadcbc18576 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/agPrimaryColsList.ts
@@ -37,6 +37,7 @@ import type { ToolPanelColumnCompParams } from './columnToolPanel';
 import { selectAllChildren } from './modelItemUtils';
 import { ToolPanelColumnComp } from './toolPanelColumnComp';
 import { ToolPanelColumnGroupComp } from './toolPanelColumnGroupComp';
+import { isDeferredMode } from './toolPanelDeferredUiUtils';
 
 class UIColumnModel implements VirtualListModel {
     constructor(private readonly items: ColumnModelItem[]) {}
@@ -120,7 +121,7 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
 
         this.expandGroupsByDefault = !contractColumnSelection;
 
-        const isPreventMove = suppressColumnMove || suppressSyncLayoutWithGrid;
+        const isPreventMove = suppressColumnMove || (suppressSyncLayoutWithGrid && !isDeferredMode(params));
 
         const virtualList = this.createManagedBean(
             new VirtualList<ToolPanelColumnGroupComp | ToolPanelColumnComp, ColumnModelItem>({
@@ -261,7 +262,8 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
         const expandedStates = this.getExpandedStates();
 
         const pivotModeActive = this.colModel.isPivotMode();
-        const shouldSyncColumnLayoutWithGrid = !params.suppressSyncLayoutWithGrid && !pivotModeActive;
+        const deferApply = isDeferredMode(params);
+        const shouldSyncColumnLayoutWithGrid = (!params.suppressSyncLayoutWithGrid || deferApply) && !pivotModeActive;
 
         if (shouldSyncColumnLayoutWithGrid) {
             this.buildTreeFromWhatGridIsDisplaying();
@@ -333,14 +335,18 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
     }
 
     private buildTreeFromWhatGridIsDisplaying(): void {
-        const deferApply = !!this.params.deferApply;
-        const columnOrder = this.beans.columnStateUpdateStrategy.getPrimaryColumns(deferApply);
-
-        if (deferApply && columnOrder.length > 0) {
-            syncLayoutWithColumns(columnOrder, this.setColumnLayout.bind(this));
+        const deferApply = isDeferredMode(this.params);
+        if (deferApply && this.beans.columnStateUpdateStrategy.hasDeferredColumnOrder(deferApply)) {
+            const columnOrder = this.beans.columnStateUpdateStrategy.getPrimaryColumns(deferApply);
+            if (columnOrder.length > 0) {
+                syncLayoutWithColumns(columnOrder, this.setColumnLayout.bind(this));
+                return;
+            }
+        }
+        if (this.params.suppressSyncLayoutWithGrid) {
+            this.buildTreeFromProvidedColumnDefs();
             return;
         }
-
         syncLayoutWithGrid(this.colModel, this.setColumnLayout.bind(this));
     }
 
@@ -572,9 +578,7 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
     }
 
     public doSetSelectedAll(selectAllChecked: boolean): void {
-        selectAllChildren(this.beans, this.allColsTree, selectAllChecked, this.eventType, {
-            deferApply: !!this.params.deferApply,
-        });
+        selectAllChildren(this.beans, this.allColsTree, selectAllChecked, this.eventType, this.params);
         this.syncVisibleSelectionState();
         this.fireSelectionChangedEvent();
     }
@@ -591,7 +595,7 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
         let uncheckedCount = 0;
 
         const updateStrategy = this.beans.columnStateUpdateStrategy;
-        const pivotMode = updateStrategy.getPivotMode(!!this.params.deferApply);
+        const pivotMode = updateStrategy.getPivotMode(isDeferredMode(this.params));
 
         this.forEachItem((item) => {
             if (item.group) {
@@ -613,13 +617,13 @@ export class AgPrimaryColsList extends Component<AgPrimaryColsListEvent> {
                     return;
                 }
                 checked =
-                    updateStrategy.isColumnSelectedInPivotModeToolPanel(!!this.params.deferApply, column) ?? false;
+                    updateStrategy.isColumnSelectedInPivotModeToolPanel(isDeferredMode(this.params), column) ?? false;
             } else {
                 if (colDef.lockVisible) {
                     return;
                 }
 
-                checked = updateStrategy.isColumnVisibleInToolPanel(!!this.params.deferApply, column) ?? false;
+                checked = updateStrategy.isColumnVisibleInToolPanel(isDeferredMode(this.params), column) ?? false;
             }
 
             if (checked) {
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts b/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts
index 8aef7f99498..ffd426f96d5 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils.ts
@@ -10,7 +10,7 @@ import { isProvidedColumnGroup } from 'ag-grid-community';
 import type { VirtualListDragItem } from '../agStack/iVirtualListDragFeature';
 import type { ToolPanelColumnComp } from './toolPanelColumnComp';
 import { ToolPanelColumnGroupComp } from './toolPanelColumnGroupComp';
-import { refreshDeferredToolPanelUi } from './toolPanelDeferredUiUtils';
+import { isDeferredMode, refreshDeferredToolPanelUi } from './toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from './updates/columnStateUpdateTypes';
 
 export const getCurrentColumnsBeingMoved = (column: AgColumn | AgProvidedColumnGroup | null): AgColumn[] => {
@@ -96,7 +96,7 @@ export const moveItem = (
     const targetIndex: number | null = getMoveTargetIndex(beans, currentColumns, lastHoveredColumn, isBefore);
 
     if (targetIndex != null) {
-        beans.columnStateUpdateStrategy.moveColumns(!!params?.deferApply, currentColumns, targetIndex, 'toolPanelUi');
+        beans.columnStateUpdateStrategy.moveColumns(isDeferredMode(params), currentColumns, targetIndex, 'toolPanelUi');
         refreshDeferredToolPanelUi(beans, params);
     }
 };
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css b/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css
index d14cc037d71..6e26c0846ec 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.css
@@ -5,14 +5,6 @@
     flex: 1 1 auto;
 }
 
-:where(.ag-column-panel.ag-column-panel-deferred),
-:where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-panel-column-select),
-:where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-drop-vertical),
-:where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-panel-defer-mode-toggle),
-:where(.ag-column-panel.ag-column-panel-deferred) :where(.ag-column-panel-buttons) {
-    background-color: var(--ag-cell-batch-edit-background-color);
-}
-
 .ag-pivot-mode-panel {
     height: var(--ag-header-height);
     display: flex;
@@ -47,27 +39,29 @@
 
 .ag-column-panel-buttons {
     display: flex;
-    flex-wrap: wrap;
-    gap: var(--ag-widget-vertical-spacing) var(--ag-widget-horizontal-spacing);
     justify-content: flex-end;
     overflow: hidden;
-    padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding);
-    border-top: var(--ag-tool-panel-separator-border);
-}
+    padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding) 0;
+    flex-wrap: wrap;
+    gap: var(--ag-widget-vertical-spacing) var(--ag-widget-horizontal-spacing);
 
-.ag-column-panel-defer-mode-toggle {
-    display: flex;
-    align-items: center;
-    gap: var(--ag-widget-horizontal-spacing);
-    padding: var(--ag-widget-container-vertical-padding) var(--ag-widget-container-horizontal-padding);
-    border-top: var(--ag-tool-panel-separator-border);
+    :where(.ag-standard-button) {
+        transition:
+            background-color 0.25s ease-in-out,
+            color 0.25s ease-in-out;
+    }
 }
 
-.ag-column-panel-defer-mode-select {
-    display: flex;
-    align-items: center;
+.ag-column-panel-buttons:where(:last-child) {
+    padding-bottom: var(--ag-widget-container-vertical-padding);
 }
 
 .ag-column-panel-buttons-button {
     line-height: 1.5;
 }
+
+/* stylelint-disable-next-line selector-max-specificity */
+:where(.ag-column-panel) .ag-column-panel-buttons-apply-button {
+    color: var(--ag-column-panel-apply-button-color);
+    background-color: var(--ag-column-panel-apply-button-background-color);
+}
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.ts b/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.ts
index b30762befd5..18abd4fe0b8 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/columnToolPanel.ts
@@ -2,23 +2,14 @@ import type {
     BeanCollection,
     ColDef,
     ColGroupDef,
+    ColumnToolPanelAction,
     ColumnToolPanelState,
-    ElementParams,
-    GridCheckbox,
     IColumnToolPanel,
     IToolPanelColumnCompParams,
     IToolPanelComp,
     IToolPanelParams,
 } from 'ag-grid-community';
-import {
-    AgToggleButtonSelector,
-    Component,
-    FilterButtonComp,
-    RefPlaceholder,
-    _addGridCommonParams,
-    _clearElement,
-    _last,
-} from 'ag-grid-community';
+import { Component, FilterButtonComp, _addGridCommonParams, _clearElement, _last, _warn } from 'ag-grid-community';
 
 import type { PivotDropZonePanel } from '../rowGrouping/columnDropZones/pivotDropZonePanel';
 import type { RowGroupDropZonePanel } from '../rowGrouping/columnDropZones/rowGroupDropZonePanel';
@@ -27,6 +18,7 @@ import { AgPrimaryCols } from './agPrimaryCols';
 import columnToolPanelCSS from './columnToolPanel.css';
 import type { ColumnToolPanelFactory } from './columnToolPanelFactory';
 import type { PivotModePanel } from './pivotModePanel';
+import { isDeferredMode } from './toolPanelDeferredUiUtils';
 
 export interface ToolPanelColumnCompParams<TData = any, TContext = any>
     extends IToolPanelParams<TData, TContext, ColumnToolPanelState>,
@@ -34,44 +26,6 @@ export interface ToolPanelColumnCompParams<TData = any, TContext = any>
 
 const DEFERRED_TOOL_PANEL_CLASS = 'ag-column-panel-deferred';
 
-const DeferModeToggleElement: ElementParams = {
-    tag: 'div',
-    cls: 'ag-column-panel-defer-mode-toggle',
-    children: [
-        {
-            tag: 'ag-toggle-button',
-            ref: 'cbDeferMode',
-            cls: 'ag-column-panel-defer-mode-select',
-        },
-    ],
-};
-
-class DeferModeToggleComp extends Component {
-    private readonly cbDeferMode: GridCheckbox = RefPlaceholder;
-
-    constructor(
-        private readonly isDeferModeEnabled: () => boolean,
-        private readonly onDeferModeChanged: (nextDeferMode: boolean) => void
-    ) {
-        super();
-    }
-
-    public postConstruct(): void {
-        this.setTemplate(DeferModeToggleElement, [AgToggleButtonSelector]);
-        const cbDeferMode = this.cbDeferMode;
-
-        cbDeferMode.setValue(this.isDeferModeEnabled());
-        cbDeferMode.setLabel(this.getLocaleTextFunc()('deferMode', 'Defer mode'));
-        this.addManagedListeners(cbDeferMode, {
-            fieldValueChanged: () => this.onDeferModeChanged(!!cbDeferMode.getValue()),
-        });
-    }
-
-    public sync(value: boolean): void {
-        this.cbDeferMode.setValue(value);
-    }
-}
-
 export class ColumnToolPanel extends Component implements IColumnToolPanel, IToolPanelComp {
     private initialised = false;
     private params: ToolPanelColumnCompParams;
@@ -84,10 +38,9 @@ export class ColumnToolPanel extends Component implements IColumnToolPanel, IToo
     private valuesDropZonePanel?: ValuesDropZonePanel;
     private pivotDropZonePanel?: PivotDropZonePanel;
     private colToolPanelFactory?: ColumnToolPanelFactory;
-    private deferModeToggleComp?: DeferModeToggleComp;
     private deferredButtonsComp?: FilterButtonComp;
-    private deferredButtonDefs: Array<{ type: 'cancel' | 'apply'; label: string }> = [];
     private isDeferModeEnabled = false;
+    private isCommitting = false;
 
     constructor() {
         super({ tag: 'div', cls: 'ag-column-panel' });
@@ -118,7 +71,6 @@ export class ColumnToolPanel extends Component implements IColumnToolPanel, IToo
             suppressValues: false,
             suppressPivots: false,
             suppressSyncLayoutWithGrid: false,
-            deferApply: false,
         });
         const mergedParams = {
             ...defaultParams,
@@ -131,7 +83,7 @@ export class ColumnToolPanel extends Component implements IColumnToolPanel, IToo
         const hasPivotModule = gos.isModuleRegistered('SharedPivot');
         const hasRowGroupingModule = hasPivotModule || gos.isModuleRegistered('SharedRowGrouping');
 
-        this.isDeferModeEnabled = !!mergedParams.deferApply;
+        this.isDeferModeEnabled = isDeferredMode(mergedParams);
         this.toggleCss(DEFERRED_TOOL_PANEL_CLASS, this.isDeferModeEnabled);
 
         if (!mergedParams.suppressPivotMode && colToolPanelFactory && hasPivotModule) {
@@ -179,39 +131,51 @@ export class ColumnToolPanel extends Component implements IColumnToolPanel, IToo
             childDestroyFuncs.push(() => pivotModeListener());
         }
 
-        if (this.params.deferApply) {
-            this.initDeferredButtons();
+        if (this.isDeferModeEnabled) {
+            const resetListener = this.onExternalGridChange;
+
+            childDestroyFuncs.push(
+                ...this.addManagedEventListeners({
+                    sortChanged: resetListener,
+                    columnVisible: resetListener,
+                    columnRowGroupChanged: resetListener,
+                    columnValueChanged: resetListener,
+                    columnPivotChanged: resetListener,
+                    columnPivotModeChanged: resetListener,
+                    newColumnsLoaded: resetListener,
+                    ...(mergedParams.suppressSyncLayoutWithGrid ? {} : { columnMoved: resetListener }),
+                })
+            );
+        }
+
+        if (mergedParams.buttons?.length) {
+            if (!mergedParams.buttons.includes('apply')) {
+                _warn(298);
+            }
+            this.initDeferredButtons(mergedParams.buttons);
         }
 
         this.initialised = true;
     }
 
-    private initDeferredButtons(): void {
-        this.deferModeToggleComp = this.createBean(
-            new DeferModeToggleComp(
-                () => this.isDeferModeEnabled,
-                (nextDeferMode) => this.onDeferModeChanged(nextDeferMode)
-            )
-        );
-        this.appendChild(this.deferModeToggleComp);
-
+    private initDeferredButtons(buttons: ColumnToolPanelAction[]): void {
         const buttonComp = this.createBean(new FilterButtonComp({ className: 'ag-column-panel-buttons' }));
         this.deferredButtonsComp = buttonComp;
         this.childDestroyFuncs.push(() => {
-            this.deferModeToggleComp = this.destroyBean(this.deferModeToggleComp);
             this.deferredButtonsComp = this.destroyBean(this.deferredButtonsComp);
         });
 
         const translate = this.getLocaleTextFunc();
 
-        this.deferredButtonDefs = (['cancel', 'apply'] as const).map((type) => ({
+        const buttonDefs = buttons.map((type) => ({
             type,
             label: translate(
                 type === 'apply' ? 'applyColumnToolPanel' : 'cancelColumnToolPanel',
                 type === 'apply' ? 'Apply' : 'Cancel'
             ),
         }));
-        buttonComp.updateButtons(this.isDeferModeEnabled ? this.deferredButtonDefs : []);
+        buttonComp.updateButtons(buttonDefs);
+        buttonComp.updateValidity(false);
         buttonComp.addManagedListeners(buttonComp, {
             apply: this.onDeferredApply,
             cancel: this.onDeferredCancel,
@@ -221,30 +185,18 @@ export class ColumnToolPanel extends Component implements IColumnToolPanel, IToo
     }
 
     private readonly onDeferredApply = (): void => {
-        this.beans.columnStateUpdateStrategy.commit(this.isDeferModeEnabled);
-    };
-
-    private readonly onDeferModeChanged = (nextDeferMode: boolean): void => {
-        if (nextDeferMode === this.isDeferModeEnabled) {
-            return;
+        this.isCommitting = true;
+        try {
+            this.beans.columnStateUpdateStrategy.commit(this.isDeferModeEnabled);
+        } finally {
+            this.isCommitting = false;
         }
-
-        if (this.isDeferModeEnabled) {
-            this.beans.columnStateUpdateStrategy.reset(this.isDeferModeEnabled);
-        }
-
-        this.isDeferModeEnabled = nextDeferMode;
-        this.params.deferApply = nextDeferMode;
-        this.toggleCss(DEFERRED_TOOL_PANEL_CLASS, nextDeferMode);
-        this.deferModeToggleComp?.sync(nextDeferMode);
-        this.deferredButtonsComp?.updateButtons(nextDeferMode ? this.deferredButtonDefs : []);
-
-        this.refreshToolPanelLayouts();
-        this.pivotModePanel?.refreshEditStrategy();
+        this.deferredButtonsComp?.updateValidity(false);
     };
 
     private readonly onDeferredCancel = (): void => {
         this.beans.columnStateUpdateStrategy.reset(this.isDeferModeEnabled);
+        this.deferredButtonsComp?.updateValidity(false);
         this.refreshToolPanelLayouts();
         this.pivotModePanel?.refreshEditStrategy();
     };
@@ -252,12 +204,31 @@ export class ColumnToolPanel extends Component implements IColumnToolPanel, IToo
     private readonly onPivotModePanelValueChanged = (): void => {
         this.refreshToolPanelLayouts();
         this.setLastVisible();
+        this.deferredButtonsComp?.updateValidity(
+            this.beans.columnStateUpdateStrategy.hasPendingChanges(this.isDeferModeEnabled)
+        );
+    };
+
+    private readonly onExternalGridChange = (): void => {
+        if (!this.isDeferModeEnabled || this.isCommitting) {
+            return;
+        }
+        if (!this.beans.columnStateUpdateStrategy.hasPendingChanges(this.isDeferModeEnabled)) {
+            return;
+        }
+        this.beans.columnStateUpdateStrategy.reset(this.isDeferModeEnabled);
+        this.deferredButtonsComp?.updateValidity(false);
+        this.refreshToolPanelLayouts();
+        this.pivotModePanel?.refreshEditStrategy();
     };
 
     public refreshDeferredUi(): void {
         this.refreshToolPanelLayouts();
         this.setLastVisible();
         this.pivotModePanel?.refreshEditStrategy();
+        this.deferredButtonsComp?.updateValidity(
+            this.beans.columnStateUpdateStrategy.hasPendingChanges(this.isDeferModeEnabled)
+        );
     }
 
     private refreshToolPanelLayouts(): void {
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/modelItemUtils.ts b/packages/ag-grid-enterprise/src/columnToolPanel/modelItemUtils.ts
index c3d54e004e1..d446455edfc 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/modelItemUtils.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/modelItemUtils.ts
@@ -8,7 +8,7 @@ import type {
 } from 'ag-grid-community';
 
 import type { ColumnModelItem } from './columnModelItem';
-import { refreshDeferredToolPanelUi } from './toolPanelDeferredUiUtils';
+import { isDeferredMode, refreshDeferredToolPanelUi } from './toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from './updates/columnStateUpdateTypes';
 
 export function selectAllChildren(
@@ -30,7 +30,7 @@ export function setAllColumns(
     params: ColumnStateUpdateParams
 ): void {
     const updateStrategy = beans.columnStateUpdateStrategy;
-    const isPivotMode = updateStrategy.getPivotMode(!!params.deferApply);
+    const isPivotMode = updateStrategy.getPivotMode(isDeferredMode(params));
 
     if (isPivotMode) {
         setAllPivot(beans, cols, selectAllChecked, eventType, params);
@@ -74,7 +74,7 @@ function setAllVisible(
         if (col.getColDef().lockVisible) {
             continue;
         }
-        if (updateStrategy.isColumnVisibleInToolPanel(!!params.deferApply, col) !== visible) {
+        if (updateStrategy.isColumnVisibleInToolPanel(isDeferredMode(params), col) !== visible) {
             colStateItems.push({
                 colId: col.getId(),
                 hide: !visible,
@@ -82,7 +82,7 @@ function setAllVisible(
         }
     }
 
-    updateStrategy.applyColumnState(!!params.deferApply, colStateItems, eventType);
+    updateStrategy.applyColumnState(isDeferredMode(params), colStateItems, eventType);
     refreshDeferredToolPanelUi(beans, params);
 }
 
@@ -108,7 +108,7 @@ function setAllPivotActive(
 
     const turnOnAction = (col: AgColumn) => {
         // don't change any column that's already got a function active
-        if (updateStrategy.isColumnSelectedInPivotModeToolPanel(!!params.deferApply, col)) {
+        if (updateStrategy.isColumnSelectedInPivotModeToolPanel(isDeferredMode(params), col)) {
             return;
         }
 
@@ -133,7 +133,7 @@ function setAllPivotActive(
     };
 
     const turnOffAction = (col: AgColumn) => {
-        const isActive = updateStrategy.isColumnSelectedInPivotModeToolPanel(!!params.deferApply, col);
+        const isActive = updateStrategy.isColumnSelectedInPivotModeToolPanel(isDeferredMode(params), col);
         if (isActive) {
             colStateItems.push({
                 colId: col.getId(),
@@ -148,7 +148,7 @@ function setAllPivotActive(
 
     columns.forEach(action);
 
-    updateStrategy.applyColumnState(!!params.deferApply, colStateItems, eventType);
+    updateStrategy.applyColumnState(isDeferredMode(params), colStateItems, eventType);
     refreshDeferredToolPanelUi(beans, params);
 }
 
@@ -165,12 +165,11 @@ export function updateColumns(
             };
         };
         eventType: ColumnEventType;
-        deferApply?: boolean;
-    }
+    } & ColumnStateUpdateParams
 ): void {
     const { columns, visibleState, pivotState, eventType } = params;
     const updateStrategy = beans.columnStateUpdateStrategy;
-    const isPivotMode = updateStrategy.getPivotMode(!!params.deferApply);
+    const isPivotMode = updateStrategy.getPivotMode(isDeferredMode(params));
     const state: ColumnState[] = columns.map((column) => {
         const colId = column.getColId();
         if (isPivotMode) {
@@ -188,7 +187,7 @@ export function updateColumns(
             };
         }
     });
-    updateStrategy.applyColumnState(!!params.deferApply, state, eventType);
+    updateStrategy.applyColumnState(isDeferredMode(params), state, eventType);
     refreshDeferredToolPanelUi(beans, params);
 }
 
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/pivotModePanel.ts b/packages/ag-grid-enterprise/src/columnToolPanel/pivotModePanel.ts
index 98e8fce70c1..ee63b3d2c0c 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/pivotModePanel.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/pivotModePanel.ts
@@ -1,6 +1,7 @@
 import type { ElementParams, GridCheckbox } from 'ag-grid-community';
 import { AgToggleButtonSelector, Component, RefPlaceholder } from 'ag-grid-community';
 
+import { isDeferredMode } from './toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from './updates/columnStateUpdateTypes';
 
 const PivotModePanelElement: ElementParams = {
@@ -25,7 +26,7 @@ export class PivotModePanel extends Component {
     }
 
     private getCurrentPivotMode(): boolean {
-        return this.beans.columnStateUpdateStrategy.getPivotMode(!!this.params.deferApply);
+        return this.beans.columnStateUpdateStrategy.getPivotMode(isDeferredMode(this.params));
     }
 
     public syncFromGrid(): void {
@@ -47,7 +48,7 @@ export class PivotModePanel extends Component {
 
         const onBtPivotMode = () => {
             const newValue = !!cbPivotMode.getValue();
-            this.beans.columnStateUpdateStrategy.setPivotMode(!!this.params.deferApply, newValue, 'toolPanelUi');
+            this.beans.columnStateUpdateStrategy.setPivotMode(isDeferredMode(this.params), newValue, 'toolPanelUi');
             this.onPivotModeValueChanged?.();
         };
 
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnComp.ts b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnComp.ts
index d863ee01342..416f4d03029 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnComp.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnComp.ts
@@ -28,6 +28,7 @@ import type { ColumnModelItem } from './columnModelItem';
 import type { ToolPanelColumnCompParams } from './columnToolPanel';
 import { createPivotStateForToolPanel, setAllColumns, updateColumns } from './modelItemUtils';
 import { ToolPanelContextMenu } from './toolPanelContextMenu';
+import { isDeferredMode } from './toolPanelDeferredUiUtils';
 
 const ToolPanelColumnElement: ElementParams = {
     tag: 'div',
@@ -161,11 +162,7 @@ export class ToolPanelColumnComp extends Component {
             return;
         }
 
-        const contextMenu = this.createBean(
-            new ToolPanelContextMenu(column, e, this.focusWrapper, {
-                deferApply: !!this.params.deferApply,
-            })
-        );
+        const contextMenu = this.createBean(new ToolPanelContextMenu(column, e, this.focusWrapper, this.params));
         this.addDestroyFunc(() => {
             if (contextMenu.isAlive()) {
                 this.destroyBean(contextMenu);
@@ -209,9 +206,7 @@ export class ToolPanelColumnComp extends Component {
             return;
         }
 
-        setAllColumns(this.beans, [this.column], nextState, 'toolPanelUi', {
-            deferApply: !!this.params.deferApply,
-        });
+        setAllColumns(this.beans, [this.column], nextState, 'toolPanelUi', this.params);
     }
 
     private refreshAriaLabel(): void {
@@ -263,7 +258,7 @@ export class ToolPanelColumnComp extends Component {
                         visibleState: dragItem?.visibleState,
                         pivotState: dragItem?.pivotState,
                         eventType: 'toolPanelUi',
-                        deferApply: this.params.deferApply,
+                        buttons: this.params.buttons,
                     });
                 }
             },
@@ -285,7 +280,7 @@ export class ToolPanelColumnComp extends Component {
         const visibleState = { [colId]: this.column.isVisible() };
         const updateStrategy = this.beans.columnStateUpdateStrategy;
         const pivotState = {
-            [colId]: createPivotStateForToolPanel(this.column, updateStrategy, !!this.params.deferApply),
+            [colId]: createPivotStateForToolPanel(this.column, updateStrategy, isDeferredMode(this.params)),
         };
         return {
             columns: [this.column],
@@ -297,17 +292,17 @@ export class ToolPanelColumnComp extends Component {
     private onColumnStateChanged(): void {
         this.processingColumnStateChange = true;
         const updateStrategy = this.beans.columnStateUpdateStrategy;
-        const isPivotMode = updateStrategy.getPivotMode(!!this.params.deferApply);
+        const isPivotMode = updateStrategy.getPivotMode(isDeferredMode(this.params));
         if (isPivotMode) {
             // if reducing, checkbox means column is one of pivot, value or group
             const anyFunctionActive = updateStrategy.isColumnSelectedInPivotModeToolPanel(
-                !!this.params.deferApply,
+                isDeferredMode(this.params),
                 this.column
             );
             this.cbSelect.setValue(anyFunctionActive);
         } else {
             // if not reducing, the checkbox tells us if column is visible or not
-            this.cbSelect.setValue(updateStrategy.isColumnVisibleInToolPanel(!!this.params.deferApply, this.column));
+            this.cbSelect.setValue(updateStrategy.isColumnVisibleInToolPanel(isDeferredMode(this.params), this.column));
         }
 
         let canBeToggled = true;
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnGroupComp.ts b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnGroupComp.ts
index c11958eaf8c..1a01bfdd930 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnGroupComp.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelColumnGroupComp.ts
@@ -31,6 +31,7 @@ import {
 import type { ColumnModelItem } from './columnModelItem';
 import { createPivotStateForToolPanel, selectAllChildren, updateColumns } from './modelItemUtils';
 import { ToolPanelContextMenu } from './toolPanelContextMenu';
+import { isDeferredMode } from './toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from './updates/columnStateUpdateTypes';
 
 const ToolPanelColumnGroupElement: ElementParams = {
@@ -243,7 +244,7 @@ export class ToolPanelColumnGroupComp extends Component {
                         visibleState: dragItem?.visibleState,
                         pivotState: dragItem?.pivotState,
                         eventType: this.eventType,
-                        deferApply: this.params.deferApply,
+                        buttons: this.params.buttons,
                     });
                 }
             },
@@ -271,7 +272,7 @@ export class ToolPanelColumnGroupComp extends Component {
             };
         } = {};
         const updateStrategy = this.beans.columnStateUpdateStrategy;
-        const deferApply = !!this.params.deferApply;
+        const deferApply = isDeferredMode(this.params);
         for (const col of columns) {
             const colId = col.getId();
             visibleState[colId] = col.isVisible();
@@ -369,7 +370,7 @@ export class ToolPanelColumnGroupComp extends Component {
 
     private workOutSelectedValue(): boolean | undefined {
         const updateStrategy = this.beans.columnStateUpdateStrategy;
-        const pivotMode = updateStrategy.getPivotMode(!!this.params.deferApply);
+        const pivotMode = updateStrategy.getPivotMode(isDeferredMode(this.params));
 
         const visibleLeafColumns = this.getVisibleLeafColumns();
 
@@ -394,7 +395,7 @@ export class ToolPanelColumnGroupComp extends Component {
     }
 
     private workOutReadOnlyValue(): boolean {
-        const pivotMode = this.beans.columnStateUpdateStrategy.getPivotMode(!!this.params.deferApply);
+        const pivotMode = this.beans.columnStateUpdateStrategy.getPivotMode(isDeferredMode(this.params));
 
         let colsThatCanAction = 0;
 
@@ -413,10 +414,10 @@ export class ToolPanelColumnGroupComp extends Component {
 
     private isColumnChecked(column: AgColumn): boolean {
         const updateStrategy = this.beans.columnStateUpdateStrategy;
-        if (updateStrategy.getPivotMode(!!this.params.deferApply)) {
-            return updateStrategy.isColumnSelectedInPivotModeToolPanel(!!this.params.deferApply, column);
+        if (updateStrategy.getPivotMode(isDeferredMode(this.params))) {
+            return updateStrategy.isColumnSelectedInPivotModeToolPanel(isDeferredMode(this.params), column);
         }
-        return updateStrategy.isColumnVisibleInToolPanel(!!this.params.deferApply, column);
+        return updateStrategy.isColumnVisibleInToolPanel(isDeferredMode(this.params), column);
     }
 
     private onExpandOrContractClicked(): void {
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelContextMenu.ts b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelContextMenu.ts
index f7bb6884207..063f33e3f0a 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelContextMenu.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelContextMenu.ts
@@ -3,7 +3,7 @@ import { Component, _createIconNoSpan, _focusInto, isColumn, isProvidedColumnGro
 
 import { getGroupingLocaleText, isRowGroupColLocked } from '../rowGrouping/rowGroupingUtils';
 import { MenuList } from '../widgets/menuList';
-import { refreshDeferredToolPanelUi } from './toolPanelDeferredUiUtils';
+import { isDeferredMode, refreshDeferredToolPanelUi } from './toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from './updates/columnStateUpdateTypes';
 
 type MenuItemName = 'scrollIntoView' | 'rowGroup' | 'value' | 'pivot';
@@ -78,7 +78,7 @@ export class ToolPanelContextMenu extends Component {
         }
         this.columns = columns;
 
-        const isPivotMode = updateStrategy.getPivotMode(!!this.params.deferApply);
+        const isPivotMode = updateStrategy.getPivotMode(isDeferredMode(this.params));
 
         this.allowScrollIntoView = !isPivotMode && columns.some(this.isColumnValidForScrollIntoView);
         this.allowGrouping = columns.some((col) => col.isPrimary() && col.isAllowRowGroup());
@@ -94,7 +94,7 @@ export class ToolPanelContextMenu extends Component {
         const menuItemMap = new Map<MenuItemName, MenuItemProperty>();
         this.menuItemMap = menuItemMap;
 
-        const deferMode = !!this.params.deferApply;
+        const deferMode = isDeferredMode(this.params);
         const isPivotMode = updateStrategy.getPivotMode(deferMode);
         const rowGroupColIdSet = new Set(
             updateStrategy.getRowGroupColumns(deferMode).map((col: AgColumn) => col.getColId())
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelDeferredUiUtils.ts b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelDeferredUiUtils.ts
index 1c6076b7744..3601eae1254 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelDeferredUiUtils.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/toolPanelDeferredUiUtils.ts
@@ -3,8 +3,12 @@ import type { BeanCollection } from 'ag-grid-community';
 import type { ColumnToolPanel } from './columnToolPanel';
 import type { ColumnStateUpdateParams } from './updates/columnStateUpdateTypes';
 
+export function isDeferredMode(params?: ColumnStateUpdateParams): boolean {
+    return !!params?.buttons?.includes('apply');
+}
+
 export function refreshDeferredToolPanelUi(beans: BeanCollection, params?: ColumnStateUpdateParams): void {
-    if (!params?.deferApply) {
+    if (!isDeferredMode(params)) {
         return;
     }
 
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateExecutionStrategy.ts b/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateExecutionStrategy.ts
index 8fbbbbf675c..5cb0ff17fc5 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateExecutionStrategy.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateExecutionStrategy.ts
@@ -7,7 +7,7 @@ import type {
     IColumnStateUpdateStrategy,
     SortDef,
 } from 'ag-grid-community';
-import { BeanStub, _applyColumnState, isColumnGroupAutoCol, isSpecialCol } from 'ag-grid-community';
+import { BeanStub, _applyColumnState, _areEqual, isColumnGroupAutoCol, isSpecialCol } from 'ag-grid-community';
 
 import type {
     ColumnStateConcreteUpdateStrategy,
@@ -30,6 +30,9 @@ export class ColumnStateUpdateExecutionStrategy extends BeanStub implements ICol
     public commit(deferMode: boolean): void {
         this.getUpdateStrategy(deferMode).commit();
     }
+    public hasPendingChanges(deferMode: boolean): boolean {
+        return this.getUpdateStrategy(deferMode).hasPendingChanges();
+    }
     public moveColumns(deferMode: boolean, columns: AgColumn[], targetIndex: number, eventType: ColumnEventType): void {
         this.getUpdateStrategy(deferMode).moveColumns(columns, targetIndex, eventType);
     }
@@ -56,6 +59,9 @@ export class ColumnStateUpdateExecutionStrategy extends BeanStub implements ICol
     public getPrimaryColumns(deferMode: boolean): AgColumn[] {
         return this.getUpdateStrategy(deferMode).getPrimaryColumns();
     }
+    public hasDeferredColumnOrder(deferMode: boolean): boolean {
+        return this.getUpdateStrategy(deferMode).hasDeferredColumnOrder();
+    }
     public setValueColumns(deferMode: boolean, columns: AgColumn[], eventType: ColumnEventType): void {
         this.getUpdateStrategy(deferMode).setValueColumns(columns, eventType);
     }
@@ -108,17 +114,15 @@ export class ColumnStateUpdateExecutionStrategy extends BeanStub implements ICol
     }
 }
 
-export function createSyncColumnStateUpdateExecutionStrategy(beans: BeanCollection): ColumnStateConcreteUpdateStrategy {
-    return new SynchronousColumnStateUpdateStrategy(beans);
-}
-
-class SynchronousColumnStateUpdateStrategy implements ColumnStateConcreteUpdateStrategy {
+export class SynchronousColumnStateUpdateStrategy implements ColumnStateConcreteUpdateStrategy {
     private lastPivotColIds: string[] = [];
 
     constructor(private readonly beans: StrategyBeans) {}
 
     public reset = noop;
     public commit = noop;
+    public hasPendingChanges = () => false;
+    public hasDeferredColumnOrder = () => false;
 
     public applyColumnState(state: ColumnState[], eventType: ColumnEventType): void {
         if (state.length === 0) {
@@ -246,6 +250,79 @@ class DeferredColumnStateUpdateStrategy implements ColumnStateConcreteUpdateStra
         this.state = {};
     }
 
+    public hasPendingChanges(): boolean {
+        const { state, beans } = this;
+        const { columnState, columnOrder, rowGroup, aggregation, pivot, pivotMode, sort, aggFuncs } = state;
+        const getColIds = (cols: AgColumn[] | undefined) => (cols ?? []).map((c) => c.getColId());
+
+        if (columnState) {
+            for (const [colId, patch] of columnState.patches) {
+                const column = beans.colModel.getColDefCol(colId);
+                if (!column) {
+                    continue;
+                }
+                if (
+                    (patch.hide !== undefined && patch.hide !== !column.isVisible()) ||
+                    (patch.rowGroup !== undefined && !!patch.rowGroup !== column.isRowGroupActive()) ||
+                    (patch.pivot !== undefined && !!patch.pivot !== column.isPivotActive()) ||
+                    (patch.aggFunc !== undefined && patch.aggFunc !== column.getAggFunc())
+                ) {
+                    return true;
+                }
+            }
+        }
+
+        if (columnOrder && !_areEqual(columnOrder.colIds, getPrimaryColumnIds(beans))) {
+            return true;
+        }
+        if (rowGroup && !_areEqual(rowGroup.colIds, getColIds(beans.rowGroupColsSvc?.columns))) {
+            return true;
+        }
+        if (aggregation && !_areEqual(aggregation.colIds, getColIds(beans.valueColsSvc?.columns))) {
+            return true;
+        }
+        if (pivot && !_areEqual(pivot.colIds, getColIds(beans.pivotColsSvc?.columns))) {
+            return true;
+        }
+        if (pivotMode && pivotMode.pivotMode !== beans.colModel.isPivotMode()) {
+            return true;
+        }
+
+        if (sort) {
+            for (const [colId, sortDef] of sort.sortDefsByColId) {
+                const column = beans.colModel.getColDefCol(colId);
+                if (!column) {
+                    continue;
+                }
+                if ((sortDef?.direction ?? null) !== (column.getSortDef()?.direction ?? null)) {
+                    return true;
+                }
+            }
+            if (sort.baselineCleared) {
+                const primaryColumns = getPrimaryColumns(beans);
+                for (const col of primaryColumns) {
+                    if (!sort.sortDefsByColId.has(col.getColId()) && col.getSortDef() !== null) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        if (aggFuncs) {
+            for (const [colId, aggFunc] of aggFuncs.values) {
+                const column = beans.colModel.getColDefCol(colId);
+                if (!column) {
+                    continue;
+                }
+                if (aggFunc !== column.getAggFunc()) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
     public commit() {
         const { beans, state } = this;
         const operations: CommitOperations = [];
@@ -548,6 +625,10 @@ class DeferredColumnStateUpdateStrategy implements ColumnStateConcreteUpdateStra
         return getDraftColumns(this.beans, this.state.columnOrder?.colIds ?? getPrimaryColumnIds(this.beans));
     }
 
+    public hasDeferredColumnOrder(): boolean {
+        return !!this.state.columnOrder;
+    }
+
     public getValueColumns(): AgColumn[] {
         return getDraftColumns(
             this.beans,
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateStrategy.ts b/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateStrategy.ts
index 14fc23d1ea8..d217b5d7c62 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateStrategy.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateStrategy.ts
@@ -8,51 +8,30 @@ import type {
 } from 'ag-grid-community';
 import { BeanStub } from 'ag-grid-community';
 
-import {
-    ColumnStateUpdateExecutionStrategy,
-    createSyncColumnStateUpdateExecutionStrategy,
-} from './columnStateUpdateExecutionStrategy';
-import type { ColumnStateConcreteUpdateStrategy } from './columnStateUpdateTypes';
+import { ColumnStateUpdateExecutionStrategy } from './columnStateUpdateExecutionStrategy';
 
 export class ColumnStateUpdateStrategy extends BeanStub implements IColumnStateUpdateStrategy {
     public beanName = 'columnStateUpdateStrategy' as const;
     private executionStrategy?: ColumnStateUpdateExecutionStrategy;
-    private fallbackUpdates?: ColumnStateConcreteUpdateStrategy;
 
     public applyColumnState(deferMode: boolean, state: ColumnState[], eventType: ColumnEventType): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.applyColumnState(deferMode, state, eventType);
-            return;
-        }
-        fallback.applyColumnState(state, eventType);
+        this.delegate('applyColumnState', deferMode, state, eventType);
     }
 
     public commit(deferMode: boolean): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.commit(deferMode);
-            return;
-        }
-        fallback.commit();
+        this.delegate('commit', deferMode);
+    }
+
+    public hasPendingChanges(deferMode: boolean): boolean {
+        return this.delegate('hasPendingChanges', deferMode);
     }
 
     public moveColumns(deferMode: boolean, columns: AgColumn[], targetIndex: number, eventType: ColumnEventType): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.moveColumns(deferMode, columns, targetIndex, eventType);
-            return;
-        }
-        fallback.moveColumns(columns, targetIndex, eventType);
+        this.delegate('moveColumns', deferMode, columns, targetIndex, eventType);
     }
 
     public reset(deferMode: boolean): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.reset(deferMode);
-            return;
-        }
-        fallback.reset();
+        this.delegate('reset', deferMode);
     }
 
     public setColumnsVisible(
@@ -61,52 +40,35 @@ export class ColumnStateUpdateStrategy extends BeanStub implements IColumnStateU
         visible: boolean,
         eventType: ColumnEventType
     ): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.setColumnsVisible(deferMode, columns, visible, eventType);
-            return;
-        }
-        fallback.setColumnsVisible(columns, visible, eventType);
+        this.delegate('setColumnsVisible', deferMode, columns, visible, eventType);
     }
 
     public isColumnVisibleInToolPanel(deferMode: boolean, column: AgColumn): boolean {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy
-            ? strategy.isColumnVisibleInToolPanel(deferMode, column)
-            : fallback.isColumnVisibleInToolPanel(column);
+        return this.delegate('isColumnVisibleInToolPanel', deferMode, column);
     }
 
     public setRowGroupColumns(deferMode: boolean, columns: AgColumn[], eventType: ColumnEventType): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.setRowGroupColumns(deferMode, columns, eventType);
-            return;
-        }
-        fallback.setRowGroupColumns(columns, eventType);
+        this.delegate('setRowGroupColumns', deferMode, columns, eventType);
     }
 
     public getRowGroupColumns(deferMode: boolean): AgColumn[] {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy ? strategy.getRowGroupColumns(deferMode) : fallback.getRowGroupColumns();
+        return this.delegate('getRowGroupColumns', deferMode);
     }
 
     public getPrimaryColumns(deferMode: boolean): AgColumn[] {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy ? strategy.getPrimaryColumns(deferMode) : fallback.getPrimaryColumns();
+        return this.delegate('getPrimaryColumns', deferMode);
+    }
+
+    public hasDeferredColumnOrder(deferMode: boolean): boolean {
+        return this.delegate('hasDeferredColumnOrder', deferMode);
     }
 
     public setValueColumns(deferMode: boolean, columns: AgColumn[], eventType: ColumnEventType): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.setValueColumns(deferMode, columns, eventType);
-            return;
-        }
-        fallback.setValueColumns(columns, eventType);
+        this.delegate('setValueColumns', deferMode, columns, eventType);
     }
 
     public getValueColumns(deferMode: boolean): AgColumn[] {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy ? strategy.getValueColumns(deferMode) : fallback.getValueColumns();
+        return this.delegate('getValueColumns', deferMode);
     }
 
     public setColumnAggFunc(
@@ -115,83 +77,53 @@ export class ColumnStateUpdateStrategy extends BeanStub implements IColumnStateU
         aggFunc: string | IAggFunc | null | undefined,
         eventType: ColumnEventType
     ): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.setColumnAggFunc(deferMode, column, aggFunc, eventType);
-            return;
-        }
-        fallback.setColumnAggFunc(column, aggFunc, eventType);
+        this.delegate('setColumnAggFunc', deferMode, column, aggFunc, eventType);
     }
 
     public getColumnAggFunc(deferMode: boolean, column: AgColumn): string | IAggFunc | null | undefined {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy ? strategy.getColumnAggFunc(deferMode, column) : fallback.getColumnAggFunc(column);
+        return this.delegate('getColumnAggFunc', deferMode, column);
     }
 
     public setPivotColumns(deferMode: boolean, columns: AgColumn[], eventType: ColumnEventType): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.setPivotColumns(deferMode, columns, eventType);
-            return;
-        }
-        fallback.setPivotColumns(columns, eventType);
+        this.delegate('setPivotColumns', deferMode, columns, eventType);
     }
 
     public getPivotColumns(deferMode: boolean): AgColumn[] {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy ? strategy.getPivotColumns(deferMode) : fallback.getPivotColumns();
+        return this.delegate('getPivotColumns', deferMode);
     }
 
     public setPivotMode(deferMode: boolean, pivotMode: boolean, eventType: ColumnEventType): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.setPivotMode(deferMode, pivotMode, eventType);
-            return;
-        }
-        fallback.setPivotMode(pivotMode, eventType);
+        this.delegate('setPivotMode', deferMode, pivotMode, eventType);
     }
 
     public getPivotMode(deferMode: boolean): boolean {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy ? strategy.getPivotMode(deferMode) : fallback.getPivotMode();
+        return this.delegate('getPivotMode', deferMode);
     }
 
     public isColumnSelectedInPivotModeToolPanel(deferMode: boolean, column: AgColumn): boolean {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy
-            ? strategy.isColumnSelectedInPivotModeToolPanel(deferMode, column)
-            : fallback.isColumnSelectedInPivotModeToolPanel(column);
+        return this.delegate('isColumnSelectedInPivotModeToolPanel', deferMode, column);
     }
 
     public progressSortFromEvent(deferMode: boolean, column: AgColumn, event: MouseEvent | KeyboardEvent): void {
-        const { strategy, fallback } = this.resolveUpdates();
-        if (strategy) {
-            strategy.progressSortFromEvent(deferMode, column, event);
-            return;
-        }
-        fallback.progressSortFromEvent(column, event);
+        this.delegate('progressSortFromEvent', deferMode, column, event);
     }
 
     public getSortDef(deferMode: boolean, column: AgColumn): SortDef | null {
-        const { strategy, fallback } = this.resolveUpdates();
-        return strategy ? strategy.getSortDef(deferMode, column) : fallback.getSortDef(column);
+        return this.delegate('getSortDef', deferMode, column);
     }
 
-    private getUpdateStrategy(): IColumnStateUpdateStrategy | undefined {
+    private getUpdateStrategy(): IColumnStateUpdateStrategy {
         return (this.executionStrategy ??= this.createManagedBean(new ColumnStateUpdateExecutionStrategy()));
     }
 
-    private getFallbackUpdates(): ColumnStateConcreteUpdateStrategy {
-        return (this.fallbackUpdates ??= createSyncColumnStateUpdateExecutionStrategy(this.beans));
-    }
-
-    private resolveUpdates(): {
-        strategy: IColumnStateUpdateStrategy | undefined;
-        fallback: ColumnStateConcreteUpdateStrategy;
-    } {
-        return {
-            strategy: this.getUpdateStrategy(),
-            fallback: this.getFallbackUpdates(),
-        };
+    private delegate<M extends keyof IColumnStateUpdateStrategy>(
+        method: M,
+        ...args: Parameters<IColumnStateUpdateStrategy[M]>
+    ): ReturnType<IColumnStateUpdateStrategy[M]> {
+        const strategy = this.getUpdateStrategy();
+        const fn = strategy[method].bind(strategy) as any as (
+            ...args: Parameters<IColumnStateUpdateStrategy[M]>
+        ) => ReturnType<IColumnStateUpdateStrategy[M]>;
+        return fn(...args);
     }
 }
diff --git a/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateTypes.ts b/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateTypes.ts
index 7567b66ca11..85877f05995 100644
--- a/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateTypes.ts
+++ b/packages/ag-grid-enterprise/src/columnToolPanel/updates/columnStateUpdateTypes.ts
@@ -1,10 +1,11 @@
 import type { AgColumn, ColumnEventType, ColumnState, IAggFunc, SortDef } from 'ag-grid-community';
 
-export type ColumnStateUpdateParams = { deferApply?: boolean };
+export type ColumnStateUpdateParams = { buttons?: Array<'apply' | 'cancel'> };
 
 export interface ColumnStateConcreteUpdateStrategy {
     applyColumnState(state: ColumnState[], eventType: ColumnEventType): void;
     commit(): void;
+    hasPendingChanges(): boolean;
     moveColumns(columns: AgColumn[], targetIndex: number, eventType: ColumnEventType): void;
     reset(): void;
     setColumnsVisible(columns: AgColumn[], visible: boolean, eventType: ColumnEventType): void;
@@ -12,6 +13,7 @@ export interface ColumnStateConcreteUpdateStrategy {
     setRowGroupColumns(columns: AgColumn[], eventType: ColumnEventType): void;
     getRowGroupColumns(): AgColumn[];
     getPrimaryColumns(): AgColumn[];
+    hasDeferredColumnOrder(): boolean;
     setValueColumns(columns: AgColumn[], eventType: ColumnEventType): void;
     getValueColumns(): AgColumn[];
     setColumnAggFunc(column: AgColumn, aggFunc: string | IAggFunc | null | undefined, eventType: ColumnEventType): void;
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts
index ebe179fc0f3..23713ce8666 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/baseDropZonePanel.ts
@@ -1,6 +1,7 @@
 import type { AgColumn, ColumnEventType, DragItem, DropTarget, GridDraggingEvent } from 'ag-grid-community';
 import { DragSourceType, _shouldUpdateColVisibilityAfterGroup } from 'ag-grid-community';
 
+import { isDeferredMode } from '../../columnToolPanel/toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from '../../columnToolPanel/updates/columnStateUpdateTypes';
 import type { PillDropZonePanelParams } from '../../widgets/pillDropZonePanel';
 import { PillDropZonePanel } from '../../widgets/pillDropZonePanel';
@@ -93,7 +94,7 @@ export abstract class BaseDropZonePanel extends PillDropZonePanel<DropZoneColumn
         }
         const allowedCols = columns.filter((c) => !c.getColDef().lockVisible);
         this.beans.columnStateUpdateStrategy.setColumnsVisible(
-            !!this.updateParams?.deferApply,
+            isDeferredMode(this.updateParams),
             allowedCols,
             visible,
             source
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts
index 56bd095c1a9..4d08405a8c7 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/dropZoneColumnComp.ts
@@ -10,6 +10,7 @@ import type {
 } from 'ag-grid-community';
 import { Component, DragSourceType, KeyCode, RefPlaceholder, _createElement } from 'ag-grid-community';
 
+import { isDeferredMode } from '../../columnToolPanel/toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from '../../columnToolPanel/updates/columnStateUpdateTypes';
 import { PillDragComp } from '../../widgets/pillDragComp';
 import { VirtualList } from '../../widgets/virtualList';
@@ -32,7 +33,7 @@ export class DropZoneColumnComp extends PillDragComp<AgColumn> {
         private readonly updateParams?: ColumnStateUpdateParams
     ) {
         super(dragSourceDropTarget, ghost, horizontal);
-        this.deferApply = !!updateParams?.deferApply;
+        this.deferApply = isDeferredMode(updateParams);
     }
 
     public override postConstruct(): void {
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/pivotDropZonePanel.ts b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/pivotDropZonePanel.ts
index 0d40c1348e8..7e8acc93583 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/pivotDropZonePanel.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/pivotDropZonePanel.ts
@@ -1,7 +1,7 @@
 import type { AgColumn, DragAndDropIcon, FocusableContainer, GridDraggingEvent } from 'ag-grid-community';
 import { _addFocusableContainerListener, _createIconNoSpan } from 'ag-grid-community';
 
-import { refreshDeferredToolPanelUi } from '../../columnToolPanel/toolPanelDeferredUiUtils';
+import { isDeferredMode, refreshDeferredToolPanelUi } from '../../columnToolPanel/toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from '../../columnToolPanel/updates/columnStateUpdateTypes';
 import { BaseDropZonePanel } from './baseDropZonePanel';
 
@@ -69,7 +69,7 @@ export class PivotDropZonePanel extends BaseDropZonePanel implements FocusableCo
             }
         } else {
             // in toolPanel, the pivot panel is always shown when pivot mode is on
-            this.setDisplayed(this.beans.columnStateUpdateStrategy.getPivotMode(!!this.updateParams?.deferApply));
+            this.setDisplayed(this.beans.columnStateUpdateStrategy.getPivotMode(isDeferredMode(this.updateParams)));
         }
     }
 
@@ -80,13 +80,13 @@ export class PivotDropZonePanel extends BaseDropZonePanel implements FocusableCo
         }
 
         const isActive = this.beans.columnStateUpdateStrategy
-            .getPivotColumns(!!this.updateParams?.deferApply)
+            .getPivotColumns(isDeferredMode(this.updateParams))
             .includes(column);
         return column.isAllowPivot() && (!isActive || this.isSourceEventFromTarget(draggingEvent));
     }
 
     protected updateItems(columns: AgColumn[]): void {
-        this.beans.columnStateUpdateStrategy.setPivotColumns(!!this.updateParams?.deferApply, columns, 'toolPanelUi');
+        this.beans.columnStateUpdateStrategy.setPivotColumns(isDeferredMode(this.updateParams), columns, 'toolPanelUi');
         refreshDeferredToolPanelUi(this.beans, this.updateParams);
     }
 
@@ -95,7 +95,7 @@ export class PivotDropZonePanel extends BaseDropZonePanel implements FocusableCo
     }
 
     protected getExistingItems(): AgColumn[] {
-        return this.beans.columnStateUpdateStrategy.getPivotColumns(!!this.updateParams?.deferApply);
+        return this.beans.columnStateUpdateStrategy.getPivotColumns(isDeferredMode(this.updateParams));
     }
 
     public getFocusableContainerName(): 'pivotToolbar' {
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/rowGroupDropZonePanel.ts b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/rowGroupDropZonePanel.ts
index cb5b63a949c..d182b53f388 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/rowGroupDropZonePanel.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/rowGroupDropZonePanel.ts
@@ -1,7 +1,7 @@
 import type { AgColumn, DragAndDropIcon, FocusableContainer, GridDraggingEvent } from 'ag-grid-community';
 import { _addFocusableContainerListener, _createIconNoSpan } from 'ag-grid-community';
 
-import { refreshDeferredToolPanelUi } from '../../columnToolPanel/toolPanelDeferredUiUtils';
+import { isDeferredMode, refreshDeferredToolPanelUi } from '../../columnToolPanel/toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from '../../columnToolPanel/updates/columnStateUpdateTypes';
 import { BaseDropZonePanel } from './baseDropZonePanel';
 
@@ -43,14 +43,14 @@ export class RowGroupDropZonePanel extends BaseDropZonePanel implements Focusabl
         }
 
         const isActive = this.beans.columnStateUpdateStrategy
-            .getRowGroupColumns(!!this.updateParams?.deferApply)
+            .getRowGroupColumns(isDeferredMode(this.updateParams))
             .includes(column);
         return column.isAllowRowGroup() && (!isActive || this.isSourceEventFromTarget(draggingEvent));
     }
 
     protected updateItems(columns: AgColumn[]) {
         this.beans.columnStateUpdateStrategy.setRowGroupColumns(
-            !!this.updateParams?.deferApply,
+            isDeferredMode(this.updateParams),
             columns,
             'toolPanelUi'
         );
@@ -62,7 +62,7 @@ export class RowGroupDropZonePanel extends BaseDropZonePanel implements Focusabl
     }
 
     protected getExistingItems(): AgColumn[] {
-        return this.beans.columnStateUpdateStrategy.getRowGroupColumns(!!this.updateParams?.deferApply);
+        return this.beans.columnStateUpdateStrategy.getRowGroupColumns(isDeferredMode(this.updateParams));
     }
 
     public getFocusableContainerName(): 'rowGroupToolbar' {
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/valueDropZonePanel.ts b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/valueDropZonePanel.ts
index be5d55c31aa..f3be0411dca 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/valueDropZonePanel.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/columnDropZones/valueDropZonePanel.ts
@@ -1,7 +1,7 @@
 import type { AgColumn, DragAndDropIcon, GridDraggingEvent } from 'ag-grid-community';
 import { _createIconNoSpan } from 'ag-grid-community';
 
-import { refreshDeferredToolPanelUi } from '../../columnToolPanel/toolPanelDeferredUiUtils';
+import { isDeferredMode, refreshDeferredToolPanelUi } from '../../columnToolPanel/toolPanelDeferredUiUtils';
 import type { ColumnStateUpdateParams } from '../../columnToolPanel/updates/columnStateUpdateTypes';
 import { BaseDropZonePanel } from './baseDropZonePanel';
 
@@ -42,17 +42,17 @@ export class ValuesDropZonePanel extends BaseDropZonePanel {
         }
 
         const isActive = this.beans.columnStateUpdateStrategy
-            .getValueColumns(!!this.updateParams?.deferApply)
+            .getValueColumns(isDeferredMode(this.updateParams))
             .includes(column);
         return column.isAllowValue() && (!isActive || this.isSourceEventFromTarget(draggingEvent));
     }
 
     protected updateItems(columns: AgColumn[]): void {
-        this.beans.columnStateUpdateStrategy.setValueColumns(!!this.updateParams?.deferApply, columns, 'toolPanelUi');
+        this.beans.columnStateUpdateStrategy.setValueColumns(isDeferredMode(this.updateParams), columns, 'toolPanelUi');
         refreshDeferredToolPanelUi(this.beans, this.updateParams);
     }
 
     protected getExistingItems(): AgColumn[] {
-        return this.beans.columnStateUpdateStrategy.getValueColumns(!!this.updateParams?.deferApply);
+        return this.beans.columnStateUpdateStrategy.getValueColumns(isDeferredMode(this.updateParams));
     }
 }
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts
index ac6ec8bb750..6856225c076 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/distributeGroupValue/distributorNumber.ts
@@ -11,6 +11,11 @@ import type {
 import type { DistributionStrategy } from './valueConversion';
 import { detectPrecision, isNumericLike, resolveStrategy, toNumber } from './valueConversion';
 
+interface ValueAndCount {
+    readonly value: number;
+    readonly count: number;
+}
+
 /** Distributes a numeric value to children using the chosen strategy. */
 export class DistributorNumber {
     private readonly children: readonly IRowNode[];
@@ -23,6 +28,7 @@ export class DistributorNumber {
     private readonly strategy: DistributionStrategy;
     private readonly getVal: ((params: DistributionGetValueParams) => unknown) | undefined;
     private readonly setVal: ((params: DistributionSetValueParams) => boolean) | undefined;
+    private readonly isAvg: boolean;
 
     constructor(
         private readonly params: GroupRowValueSetterParams,
@@ -33,12 +39,14 @@ export class DistributorNumber {
         const { aggregatedChildren: children, column, colDef, newValue } = params;
         const newNumber = toNumber(newValue);
         const count = children.length;
+        const isAvg = aggFunc === 'avg';
         this.children = children;
         this.column = column;
         this.count = count;
         this.newValue = newValue;
         this.strategy = resolveStrategy(aggFunc, opts?.distribution);
-        if (aggFunc === 'avg') {
+        this.isAvg = isAvg;
+        if (isAvg) {
             this.target = newNumber * count;
             this.oldTarget = toNumber(params.oldValue) * count;
         } else {
@@ -76,11 +84,12 @@ export class DistributorNumber {
             return this.writeAll(newValue);
         }
 
+        const { children } = this;
         switch (strategy) {
             case 'first':
-                return this.writeOne(0, newValue);
+                return this.writeOne(children[0], newValue);
             case 'last':
-                return this.writeOne(this.count - 1, newValue);
+                return this.writeOne(children[this.count - 1], newValue);
             case 'min':
                 return this.writeToExtremum(true);
             case 'max':
@@ -104,13 +113,12 @@ export class DistributorNumber {
             case 'increment':
                 return this.distributeIncrement();
             default:
-                return this.distributePercentage();
+                return this.isAvg ? this.distributePercentageAvg() : this.distributePercentage();
         }
     }
 
-    private readOne(index: number): number {
-        const { children, column, getVal } = this;
-        const node = children[index];
+    private readOne(node: IRowNode): number {
+        const { column, getVal } = this;
         if (getVal) {
             const { colDef, api, context } = this.params;
             return toNumber(getVal({ node, data: node.data, column, colDef, api, context, groupParams: this.params }));
@@ -118,9 +126,34 @@ export class DistributorNumber {
         return toNumber(node.getDataValue(column, 'value'));
     }
 
-    private writeOne(index: number, value: unknown): boolean {
-        const { children, column, setVal } = this;
-        const node = children[index];
+    /**
+     * Reads a child's value and leaf count in a single call.
+     * For group children, reads the raw avg agg object { value, count } if available,
+     * falling back to allLeafChildren.length. For leaf children, count is always 1.
+     */
+    private readValueAndCount(node: IRowNode): ValueAndCount {
+        const { column, getVal } = this;
+        let raw: unknown;
+        if (getVal) {
+            const { colDef, api, context } = this.params;
+            raw = getVal({ node, data: node.data, column, colDef, api, context, groupParams: this.params });
+        } else {
+            raw = node.getDataValue(column);
+        }
+        if (node.group) {
+            if (raw != null && typeof raw === 'object') {
+                const { value, count } = raw as ValueAndCount;
+                if (value != null && typeof count === 'number' && count > 0) {
+                    return typeof value === 'number' ? (raw as ValueAndCount) : { value: toNumber(value), count };
+                }
+            }
+            return { value: toNumber(raw), count: node.allLeafChildren?.length || 1 };
+        }
+        return { value: toNumber(raw), count: 1 };
+    }
+
+    private writeOne(node: IRowNode, value: unknown): boolean {
+        const { column, setVal } = this;
         if (setVal) {
             const { colDef, api, context } = this.params;
             return setVal({ node, data: node.data, column, colDef, api, context, groupParams: this.params, value });
@@ -129,10 +162,10 @@ export class DistributorNumber {
     }
 
     private writeAll(value: unknown): boolean {
-        const { count } = this;
+        const { children, count } = this;
         let changed = false;
         for (let i = 0; i < count; ++i) {
-            if (this.writeOne(i, value)) {
+            if (this.writeOne(children[i], value)) {
                 changed = true;
             }
         }
@@ -140,28 +173,25 @@ export class DistributorNumber {
     }
 
     private writeToExtremum(isMin: boolean): boolean {
-        const { count, newValue } = this;
-        let bestIdx = 0;
-        let bestVal = this.readOne(0);
+        const { children, count, newValue } = this;
+        let bestNode = children[0];
+        let bestVal = this.readOne(bestNode);
         for (let i = 1; i < count; i++) {
-            const v = this.readOne(i);
+            const node = children[i];
+            const v = this.readOne(node);
             if (isMin ? v < bestVal : v > bestVal) {
                 bestVal = v;
-                bestIdx = i;
+                bestNode = node;
             }
         }
-        return this.writeOne(bestIdx, newValue);
+        return this.writeOne(bestNode, newValue);
     }
 
     private distributeUniform(): boolean {
-        const { count, target, precision } = this;
-
-        // No rounding — write same float to every child
+        const { children, count, target, precision } = this;
         if (precision === undefined) {
             return this.writeAll(target / count);
         }
-
-        // Scaled integer division with remainder spread to first N children
         const scale = 10 ** precision;
         const intTarget = Math.round(target * scale);
         const base = Math.trunc(intTarget / count);
@@ -170,7 +200,7 @@ export class DistributorNumber {
         const step = rem >= 0 ? 1 : -1;
         let changed = false;
         for (let i = 0; i < count; ++i) {
-            if (this.writeOne(i, (i < absRem ? base + step : base) / scale)) {
+            if (this.writeOne(children[i], (i < absRem ? base + step : base) / scale)) {
                 changed = true;
             }
         }
@@ -178,21 +208,18 @@ export class DistributorNumber {
     }
 
     private distributeIncrement(): boolean {
-        const { count, target, oldTarget, precision } = this;
-
-        // No rounding — add delta / count to each child
+        const { children, count, target, oldTarget, precision } = this;
         if (precision === undefined) {
             const add = (target - oldTarget) / count;
             let changed = false;
             for (let i = 0; i < count; ++i) {
-                if (this.writeOne(i, this.readOne(i) + add)) {
+                const child = children[i];
+                if (this.writeOne(child, this.readOne(child) + add)) {
                     changed = true;
                 }
             }
             return changed;
         }
-
-        // Scaled integer delta with remainder spread to first N children
         const scale = 10 ** precision;
         const intDelta = Math.round(target * scale) - Math.round(oldTarget * scale);
         const base = Math.trunc(intDelta / count);
@@ -201,45 +228,50 @@ export class DistributorNumber {
         const step = rem >= 0 ? 1 : -1;
         let changed = false;
         for (let i = 0; i < count; ++i) {
-            const cur = Math.round(this.readOne(i) * scale);
-            if (this.writeOne(i, (cur + base + (i < absRem ? step : 0)) / scale)) {
+            const child = children[i];
+            const cur = Math.round(this.readOne(child) * scale);
+            if (this.writeOne(child, (cur + base + (i < absRem ? step : 0)) / scale)) {
                 changed = true;
             }
         }
         return changed;
     }
 
+    /**
+     * Scales each child's value proportionally so they sum to the target.
+     * Each child keeps its relative share: newValue[i] = oldValue[i] × (target / oldTotal).
+     * With precision rounding, uses scaled integers and spreads the remainder across the first N children.
+     */
     private distributePercentage(): boolean {
-        const { count, target, precision } = this;
+        const { children, count, target, precision } = this;
 
-        // Read all child values and compute total
         const values = new Array<number>(count);
         let total = 0;
         for (let i = 0; i < count; ++i) {
-            const v = this.readOne(i);
+            const v = this.readOne(children[i]);
             values[i] = v;
             total += v;
         }
 
-        // Zero total — fall back to uniform
+        // All children are zero — can't compute proportions, fall back to equal split
         if (total === 0) {
             return this.distributeUniform();
         }
 
-        // No rounding — direct float proportional scaling
+        // No precision — scale each value by the same ratio
         if (precision === undefined) {
             const ratio = target / total;
             let changed = false;
             for (let i = 0; i < count; ++i) {
-                if (this.writeOne(i, values[i] * ratio)) {
+                if (this.writeOne(children[i], values[i] * ratio)) {
                     changed = true;
                 }
             }
             return changed;
         }
 
-        // Rounding — scaled integer proportional distribution.
-        // (v / total) * intTarget avoids overflow from v * intTarget for large values.
+        // With precision — work in scaled integers to avoid floating-point drift.
+        // Round each child's share individually, then fix the remainder.
         const scale = 10 ** precision;
         const intTarget = Math.round(target * scale);
         let roundedSum = 0;
@@ -249,13 +281,86 @@ export class DistributorNumber {
             roundedSum += r;
         }
 
-        // Spread rounding remainder to first N children so the total is exact
+        // Spread rounding remainder (±1) across the first N children so the total is exact
+        const rem = intTarget - roundedSum;
+        const absRem = Math.abs(rem);
+        const step = rem >= 0 ? 1 : -1;
+        let changed = false;
+        for (let i = 0; i < count; ++i) {
+            if (this.writeOne(children[i], (values[i] + (i < absRem ? step : 0)) / scale)) {
+                changed = true;
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Percentage distribution for avg aggregation.
+     *
+     * Avg group children may themselves be groups with different leaf counts,
+     * so we can't scale avg values directly — a group averaging 2 leaves contributes
+     * twice as much to the parent avg as a group averaging 1 leaf.
+     *
+     * Instead, we convert each child's avg to its sum contribution (avg × leafCount),
+     * scale those sums proportionally, then convert back to avg for writing.
+     *
+     * The target is also converted: constructor stores target = newAvg × childGroupCount,
+     * but we need effectiveTarget = newAvg × totalLeafCount for sum-space scaling.
+     */
+    private distributePercentageAvg(): boolean {
+        const { children, count, target, precision } = this;
+
+        // Read each child's avg and leaf count in one call, convert to sum contributions
+        const values = new Array<number>(count);
+        const leafCounts = new Array<number>(count);
+        let total = 0;
+        let totalLeafCount = 0;
+        for (let i = 0; i < count; ++i) {
+            const vc = this.readValueAndCount(children[i]);
+            const lc = vc.count;
+            leafCounts[i] = lc;
+            totalLeafCount += lc;
+            const v = vc.value * lc;
+            values[i] = v;
+            total += v;
+        }
+
+        if (total === 0) {
+            return this.distributeUniform();
+        }
+
+        // Convert target from avg-space to sum-space
+        const effectiveTarget = (target / count) * totalLeafCount;
+
+        // No precision — scale sums proportionally, convert back to avg by dividing by leafCount
+        if (precision === undefined) {
+            const ratio = effectiveTarget / total;
+            let changed = false;
+            for (let i = 0; i < count; ++i) {
+                if (this.writeOne(children[i], (values[i] * ratio) / leafCounts[i])) {
+                    changed = true;
+                }
+            }
+            return changed;
+        }
+
+        // With precision — scaled integer proportional distribution in sum-space
+        const scale = 10 ** precision;
+        const intTarget = Math.round(effectiveTarget * scale);
+        let roundedSum = 0;
+        for (let i = 0; i < count; ++i) {
+            const r = Math.round((values[i] / total) * intTarget);
+            values[i] = r;
+            roundedSum += r;
+        }
+
         const rem = intTarget - roundedSum;
         const absRem = Math.abs(rem);
         const step = rem >= 0 ? 1 : -1;
         let changed = false;
         for (let i = 0; i < count; ++i) {
-            if (this.writeOne(i, (values[i] + (i < absRem ? step : 0)) / scale)) {
+            // Convert scaled integer sum back to avg: intSum / (scale × leafCount)
+            if (this.writeOne(children[i], (values[i] + (i < absRem ? step : 0)) / (scale * leafCounts[i]))) {
                 changed = true;
             }
         }
diff --git a/packages/ag-grid-enterprise/src/setFilter/flatSetDisplayValueModel.ts b/packages/ag-grid-enterprise/src/setFilter/flatSetDisplayValueModel.ts
index edf7d2d83ed..080ed799aab 100644
--- a/packages/ag-grid-enterprise/src/setFilter/flatSetDisplayValueModel.ts
+++ b/packages/ag-grid-enterprise/src/setFilter/flatSetDisplayValueModel.ts
@@ -11,8 +11,7 @@ export class FlatSetDisplayValueModel<V> implements ISetDisplayValueModel<V> {
         private readonly valueSvc: ValueService,
         private readonly getValueFormatter: () => ((params: ValueFormatterParams) => string) | undefined,
         private readonly formatter: TextFormatter,
-        private readonly column: AgColumn,
-        private readonly getUseFormatterFromColumn: () => boolean
+        private readonly column: AgColumn
     ) {}
 
     public updateDisplayedValuesToAllAvailable(
@@ -41,13 +40,7 @@ export class FlatSetDisplayValueModel<V> implements ISetDisplayValueModel<V> {
                 }
             } else {
                 const value = getValue(key);
-                const valueFormatterValue = this.valueSvc.formatValue(
-                    this.column,
-                    null,
-                    value,
-                    valueFormatter,
-                    this.getUseFormatterFromColumn()
-                );
+                const valueFormatterValue = this.valueSvc.formatValue(this.column, null, value, valueFormatter, false);
 
                 const textFormatterValue = this.formatter(valueFormatterValue);
 
diff --git a/packages/ag-grid-enterprise/src/setFilter/setFilter.ts b/packages/ag-grid-enterprise/src/setFilter/setFilter.ts
index 97dd190fd49..c96ff03ac90 100644
--- a/packages/ag-grid-enterprise/src/setFilter/setFilter.ts
+++ b/packages/ag-grid-enterprise/src/setFilter/setFilter.ts
@@ -101,8 +101,7 @@ export class SetFilter<V = string>
                   this.beans.valueSvc,
                   () => this.handler.valueFormatter,
                   this.formatter,
-                  column as AgColumn,
-                  () => this.handler.shouldUseValueFormatterFromColumn()
+                  column as AgColumn
               ) as any);
 
         handler.valueModel.allKeys.then((values) => {
@@ -431,7 +430,6 @@ export class SetFilter<V = string>
             params: this.params,
             translate: (translateKey: any) => translateForSetFilter(this, translateKey),
             valueFormatter: this.handler.valueFormatter,
-            shouldUseFormatterFromColumn: this.handler.shouldUseValueFormatterFromColumn(),
             item,
             isSelected,
             isTree,
diff --git a/packages/ag-grid-enterprise/src/setFilter/setFilterHandler.ts b/packages/ag-grid-enterprise/src/setFilter/setFilterHandler.ts
index fff288e69e2..cee16424d78 100644
--- a/packages/ag-grid-enterprise/src/setFilter/setFilterHandler.ts
+++ b/packages/ag-grid-enterprise/src/setFilter/setFilterHandler.ts
@@ -51,7 +51,6 @@ export class SetFilterHandler<TValue = string>
     private caseSensitive: boolean = false;
     public valueFormatter?: (params: ValueFormatterParams) => string;
     private noValueFormatterSupplied = false;
-    private useValueFormatterFromColumn = false;
 
     public init(params: FilterHandlerParams<any, any, SetFilterModel, ISetFilterParams<any, TValue>>): void {
         this.updateParams(params);
@@ -107,7 +106,7 @@ export class SetFilterHandler<TValue = string>
         this.params = params;
         const {
             colDef,
-            filterParams: { caseSensitive, treeList, keyCreator },
+            filterParams: { caseSensitive, treeList, keyCreator, valueFormatter },
         } = params;
         this.caseSensitive = !!caseSensitive;
         const isGroupCol = !!colDef.showRowGroup;
@@ -115,7 +114,7 @@ export class SetFilterHandler<TValue = string>
         this.groupingTreeList = !!this.beans.rowGroupColsSvc?.columns.length && !!treeList && isGroupCol;
         const resolvedKeyCreator = keyCreator ?? colDef.keyCreator;
         this.createKey = this.generateCreateKey(resolvedKeyCreator, this.isTreeDataOrGrouping());
-        this.setValueFormatter(resolvedKeyCreator, params);
+        this.setValueFormatter(valueFormatter, resolvedKeyCreator, !!treeList, !!colDef.refData);
     }
 
     public doesFilterPass(params: DoesFilterPassParams<any, SetFilterModel>): boolean {
@@ -151,12 +150,9 @@ export class SetFilterHandler<TValue = string>
 
     private getFormattedValue(key: string | null): string | null {
         let value: TValue | string | null = this.valueModel.getValueForFormatter(key);
-        if (this.isTreeDataOrGrouping() && Array.isArray(value)) {
-            const shouldUseLast = this.noValueFormatterSupplied || this.useValueFormatterFromColumn;
-            if (shouldUseLast) {
-                // essentially get back the cell value
-                value = _last(value) as string;
-            }
+        if (this.noValueFormatterSupplied && this.isTreeDataOrGrouping() && Array.isArray(value)) {
+            // essentially get back the cell value
+            value = _last(value) as string;
         }
 
         const formattedValue = this.beans.valueSvc.formatValue(
@@ -164,7 +160,7 @@ export class SetFilterHandler<TValue = string>
             null,
             value,
             this.valueFormatter,
-            this.useValueFormatterFromColumn
+            false
         );
 
         return (
@@ -405,36 +401,24 @@ export class SetFilterHandler<TValue = string>
     }
 
     private setValueFormatter(
+        providedValueFormatter: ((params: ValueFormatterParams) => string) | undefined,
         keyCreator: ((params: KeyCreatorParams<any, any>) => string) | undefined,
-        params: FilterHandlerParams<any, any, SetFilterModel, ISetFilterParams<any, TValue>>
+        treeList: boolean,
+        isRefData: boolean
     ) {
-        const {
-            colDef: { refData, valueFormatter },
-            filterParams: { treeList, valueFormatter: providedValueFormatter },
-        } = params;
-        const hasKeyCreatorButNoFormatterNorTreeList =
-            keyCreator && !(providedValueFormatter || treeList || valueFormatter);
-        if (hasKeyCreatorButNoFormatterNorTreeList) {
-            _error(249);
-            this.valueFormatter = undefined;
+        let valueFormatter = providedValueFormatter;
+        if (!valueFormatter) {
+            if (keyCreator && !treeList) {
+                _error(249);
+                return;
+            }
             this.noValueFormatterSupplied = true;
-            this.useValueFormatterFromColumn = false;
-            return;
-        }
-
-        let resolvedFormatter = providedValueFormatter;
-        if (!resolvedFormatter && !valueFormatter && !refData) {
             // ref data is handled by ValueService
-            resolvedFormatter = (params) => _toStringOrNull(params.value)!;
+            if (!isRefData) {
+                valueFormatter = (params) => _toStringOrNull(params.value)!;
+            }
         }
-
-        this.valueFormatter = resolvedFormatter;
-        this.noValueFormatterSupplied = !providedValueFormatter && !valueFormatter;
-        this.useValueFormatterFromColumn = !providedValueFormatter && !!valueFormatter;
-    }
-
-    public shouldUseValueFormatterFromColumn(): boolean {
-        return this.useValueFormatterFromColumn && !this.valueFormatter;
+        this.valueFormatter = valueFormatter;
     }
 
     public getCrossFilterModel(
diff --git a/packages/ag-grid-enterprise/src/setFilter/setFilterListItem.ts b/packages/ag-grid-enterprise/src/setFilter/setFilterListItem.ts
index de6d69647a7..0e20b6ef746 100644
--- a/packages/ag-grid-enterprise/src/setFilter/setFilterListItem.ts
+++ b/packages/ag-grid-enterprise/src/setFilter/setFilterListItem.ts
@@ -57,7 +57,6 @@ export interface SetFilterListItemParams<V> {
     params: ISetFilterParams<any, V> & FilterDisplayParams<any, any, SetFilterModel>;
     translate: (key: SetFilterLocaleTextKey) => string;
     valueFormatter?: (params: ValueFormatterParams) => string;
-    shouldUseFormatterFromColumn?: boolean;
     item: SetFilterModelTreeItem | string | null;
     isSelected: boolean | undefined;
     isTree?: boolean;
@@ -107,7 +106,6 @@ export class SetFilterListItem<V> extends Component<SetFilterListItemEvent> {
     private readonly params: ISetFilterParams<any, V> & FilterDisplayParams<any, any, SetFilterModel>;
     private readonly translate: (key: SetFilterLocaleTextKey) => string;
     private readonly valueFormatter?: (params: ValueFormatterParams) => string;
-    private readonly useFormatterFromColumn?: boolean;
     private readonly isTree?: boolean;
     private readonly depth: number;
     private readonly isGroup?: boolean;
@@ -134,7 +132,6 @@ export class SetFilterListItem<V> extends Component<SetFilterListItemEvent> {
         this.params = params.params;
         this.translate = params.translate;
         this.valueFormatter = params.valueFormatter;
-        this.useFormatterFromColumn = params.shouldUseFormatterFromColumn;
         this.item = params.item;
         this.isSelected = params.isSelected;
         this.isTree = params.isTree;
@@ -413,7 +410,7 @@ export class SetFilterListItem<V> extends Component<SetFilterListItemEvent> {
     }
 
     private getFormattedValue(column: AgColumn, value: any) {
-        return this.beans.valueSvc.formatValue(column, null, value, this.valueFormatter, !!this.useFormatterFromColumn);
+        return this.beans.valueSvc.formatValue(column, null, value, this.valueFormatter, false);
     }
 
     private renderCell(): void {
diff --git a/scripts/waitForCiRun.sh b/scripts/waitForCiRun.sh
index 4b6fadedc04..4184db6acea 100755
--- a/scripts/waitForCiRun.sh
+++ b/scripts/waitForCiRun.sh
@@ -18,3 +18,32 @@ then
   echo "CI run not initiated in time"
   exit 1
 fi
+
+# Wait for workflow to complete
+echo $RUN_ID
+gh run watch $RUN_ID --exit-status
+
+# Wait for the branch to be merged into latest
+merged_count=0
+merged_sleep=60
+merged_wait_count=$((600 / merged_sleep)) # Wait for up to 10mins
+while [[ merged_count -lt merged_wait_count ]]; do
+  MERGED=$(gh pr view $BRANCH --json mergedAt --jq '.mergedAt')
+  STATE=$(gh pr view $BRANCH --json state --jq '.state')
+
+  if [ -n "$MERGED" ] && [ "$MERGED" != "null" ]; then
+    echo "PR #$BRANCH merged!"
+    break
+  elif [ "$STATE" = "CLOSED" ]; then
+    echo "PR #$BRANCH was closed without merging!"
+    exit 1
+  fi
+
+  merged_count=$((merged_count+1))
+  sleep $merged_sleep
+done
+
+if [[ merged_count -ge merged_wait_count ]]; then
+  echo "PR #$BRANCH was not merged within the timeout period"
+  exit 1
+fi
diff --git a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts
index 533de3c25b1..56fa07ebf04 100644
--- a/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts
+++ b/testing/behavioural/src/cell-editing/group-edit/distribute-group-value/distribute-aggfunc.test.ts
@@ -393,3 +393,106 @@ describe('overriding built-in aggFunc with a custom function', () => {
         `);
     });
 });
+
+describe('avg percentage distribution on non-leaf groups', () => {
+    // Norway has 2 sub-groups: 2010 (2 leaves: 23, 19) and 2002 (1 leaf: 19)
+    // avg = (23+19+19)/3 = 20.333..., sum = 61
+    const norwayRowData = () => [
+        { id: 'n1', country: 'Norway', year: '2010', total: 23 },
+        { id: 'n2', country: 'Norway', year: '2010', total: 19 },
+        { id: 'n3', country: 'Norway', year: '2002', total: 19 },
+    ];
+
+    test('editing avg on non-leaf group with percentage distribution produces correct avg', async () => {
+        const api = await gridsManager.createGridAndWait('avg-pct-nested', {
+            defaultColDef: { cellEditor: 'agTextCellEditor' },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
+                { field: 'country', rowGroup: true, hide: true },
+                { field: 'year', rowGroup: true, hide: true },
+                {
+                    colId: 'total',
+                    field: 'total',
+                    aggFunc: 'avg',
+                    editable: true,
+                    groupRowEditable: true,
+                    groupRowValueSetter: { distribution: 'percentage', precision: 0 },
+                },
+            ],
+            rowData: norwayRowData(),
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data?.id,
+        });
+
+        const norwayNode = api.getRowNode('row-group-country-Norway')!;
+        expect(norwayNode.aggData?.total).toMatchObject({ value: expect.closeTo(20.333, 2), count: 3 });
+
+        // Edit Norway avg to 20
+        norwayNode.setDataValue('total', 20, 'ui');
+        await asyncSetTimeout(0);
+
+        // Desired sum = 20 * 3 = 60. Old sums: 2010=42, 2002=19, total=61.
+        // Percentage distributes sum proportionally: 2010 gets round(42/61*60)=41, 2002 gets round(19/61*60)=19.
+        // 2010 avg = 41/2 = 20.5, cascades to leaves: [round(23/42*41)=22, round(19/42*41)=19]
+        // 2002 avg = 19, leaf unchanged.
+        // Final: [22, 19, 19], sum=60, avg=20
+        expect(api.getRowNode('n1')?.data?.total).toBe(22);
+        expect(api.getRowNode('n2')?.data?.total).toBe(19);
+        expect(api.getRowNode('n3')?.data?.total).toBe(19);
+        expect(norwayNode.aggData?.total).toMatchObject({ value: 20, count: 3 });
+    });
+
+    test('editing avg on 3-level group hierarchy distributes correctly through all levels', async () => {
+        // Region > Country > Year, 3-level hierarchy
+        // Europe: France > 2010: [10, 20], France > 2011: [30], Germany > 2010: [40, 50]
+        // France avg = 60/3 = 20, Germany avg = 90/2 = 45, Europe avg = 150/5 = 30
+        const api = await gridsManager.createGridAndWait('avg-pct-3-level', {
+            defaultColDef: { cellEditor: 'agTextCellEditor' },
+            groupDisplayType: 'custom',
+            columnDefs: [
+                { colId: 'group', headerName: 'Group', cellRenderer: 'agGroupCellRenderer' },
+                { field: 'region', rowGroup: true, hide: true },
+                { field: 'country', rowGroup: true, hide: true },
+                { field: 'year', rowGroup: true, hide: true },
+                {
+                    colId: 'amount',
+                    field: 'amount',
+                    aggFunc: 'avg',
+                    editable: true,
+                    groupRowEditable: true,
+                    groupRowValueSetter: { distribution: 'percentage', precision: 0 },
+                },
+            ],
+            rowData: [
+                { id: 'f1', region: 'Europe', country: 'France', year: '2010', amount: 10 },
+                { id: 'f2', region: 'Europe', country: 'France', year: '2010', amount: 20 },
+                { id: 'f3', region: 'Europe', country: 'France', year: '2011', amount: 30 },
+                { id: 'g1', region: 'Europe', country: 'Germany', year: '2010', amount: 40 },
+                { id: 'g2', region: 'Europe', country: 'Germany', year: '2010', amount: 50 },
+            ],
+            groupDefaultExpanded: -1,
+            getRowId: (params) => params.data?.id,
+        });
+
+        const europeNode = api.getRowNode('row-group-region-Europe')!;
+        expect(europeNode.aggData?.amount).toMatchObject({ value: 30, count: 5 });
+
+        // Edit Europe avg from 30 to 20 (desired sum = 100, old sum = 150)
+        europeNode.setDataValue('amount', 20, 'ui');
+        await asyncSetTimeout(0);
+
+        // Level 1: France sum 60→40, Germany sum 90→60 (proportional by sum contributions)
+        // Level 2: France>2010 sum 30→20 (avg=10), France>2011 sum 30→20 (avg=20)
+        //          Germany>2010 sum 90→60 (avg=30)
+        // Level 3: France>2010 leaves [10,20]→[7,13], France>2011 [30]→[20]
+        //          Germany>2010 leaves [40,50]→[27,33]
+        // Final: [7, 13, 20, 27, 33], sum=100, avg=20
+        expect(api.getRowNode('f1')?.data?.amount).toBe(7);
+        expect(api.getRowNode('f2')?.data?.amount).toBe(13);
+        expect(api.getRowNode('f3')?.data?.amount).toBe(20);
+        expect(api.getRowNode('g1')?.data?.amount).toBe(27);
+        expect(api.getRowNode('g2')?.data?.amount).toBe(33);
+        expect(europeNode.aggData?.amount).toMatchObject({ value: 20, count: 5 });
+    });
+});
diff --git a/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts b/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts
index a6b450b5d7a..35187c97c8d 100644
--- a/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts
+++ b/testing/behavioural/src/columnToolPanel/deferred-pivot-mode.test.ts
@@ -1,4 +1,4 @@
-import { fireEvent, getByTestId, getByText } from '@testing-library/dom';
+import { fireEvent, getByTestId } from '@testing-library/dom';
 
 import type { AgColumn, ColDef, ColGroupDef, GridApi, IColumnStateUpdateStrategy } from 'ag-grid-community';
 import { DragSourceType, agTestIdFor, getGridElement, setupAgTestIds } from 'ag-grid-community';
@@ -102,7 +102,7 @@ describe('deferred column tool panel pivot mode', () => {
                         labelKey: 'columns',
                         iconKey: 'columns',
                         toolPanel: 'agColumnsToolPanel',
-                        toolPanelParams: { deferApply: true },
+                        toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                     },
                 ],
                 defaultToolPanel: 'columns',
@@ -138,7 +138,7 @@ describe('deferred column tool panel pivot mode', () => {
                         labelKey: 'columns',
                         iconKey: 'columns',
                         toolPanel: 'agColumnsToolPanel',
-                        toolPanelParams: { deferApply: true },
+                        toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                     },
                 ],
                 defaultToolPanel: 'columns',
@@ -273,7 +273,7 @@ describe('deferred column tool panel pivot mode', () => {
                         labelKey: 'columns',
                         iconKey: 'columns',
                         toolPanel: 'agColumnsToolPanel',
-                        toolPanelParams: { deferApply: true },
+                        toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                     },
                 ],
                 defaultToolPanel: 'columns',
@@ -302,10 +302,6 @@ describe('deferred column tool panel pivot mode', () => {
         )!;
     }
 
-    function getDeferModeToggle(toolPanelGui: HTMLElement): HTMLInputElement {
-        return toolPanelGui.querySelector('.ag-column-panel-defer-mode-toggle input[type="checkbox"]')!;
-    }
-
     function getPivotModeToggle(toolPanelGui: HTMLElement): HTMLInputElement {
         return getByTestId(toolPanelGui, agTestIdFor.pivotModeSelect()) as HTMLInputElement;
     }
@@ -470,7 +466,7 @@ describe('deferred column tool panel pivot mode', () => {
                 position: 'bottom',
                 component,
             },
-            { deferApply: true }
+            { buttons: ['apply', 'cancel'] as const }
         );
         await asyncSetTimeout(50);
     }
@@ -670,7 +666,7 @@ describe('deferred column tool panel pivot mode', () => {
                         labelKey: 'columns',
                         iconKey: 'columns',
                         toolPanel: 'agColumnsToolPanel',
-                        toolPanelParams: { deferApply: true },
+                        toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                     },
                 ],
                 defaultToolPanel: 'columns',
@@ -802,46 +798,6 @@ describe('deferred column tool panel pivot mode', () => {
         expect(serverGetDataSpy.mock.calls.length - initialCallCount).toBe(1);
     });
 
-    test('defer mode footer buttons hide when defer mode is turned off', async () => {
-        const { toolPanelGui } = await createDeferredNonPivotGrid();
-
-        expect(getByText(toolPanelGui, 'Apply')).toBeTruthy();
-        expect(getByText(toolPanelGui, 'Cancel')).toBeTruthy();
-
-        getDeferModeToggle(toolPanelGui).click();
-
-        expect(toolPanelGui.textContent).not.toContain('Apply');
-        expect(toolPanelGui.textContent).not.toContain('Cancel');
-    });
-
-    test('Defer mode toggle should work (toggle between deferMode and normal)', async () => {
-        const { gridApi, toolPanel, toolPanelGui } = await createDeferredNonPivotGrid();
-        const athlete = gridApi.getColumn('athlete')! as AgColumn;
-
-        getUpdateStrategy(toolPanel).moveColumns(true, [athlete], 2, 'toolPanelUi');
-        expect(getPrimaryColumnOrder(toolPanel).slice(0, 3)).toEqual(['athlete', 'age', 'country']);
-        commitChanges(toolPanel);
-        expect(getPrimaryColumnOrder(toolPanel).slice(0, 3)).toEqual(['age', 'athlete', 'country']);
-
-        getDeferModeToggle(toolPanelGui).click();
-        getUpdateStrategy(toolPanel).moveColumns(false, [athlete], 0, 'toolPanelUi');
-        expect(getPrimaryColumnOrder(toolPanel).slice(0, 3)).toEqual(['athlete', 'age', 'country']);
-
-        getDeferModeToggle(toolPanelGui).click();
-        getUpdateStrategy(toolPanel).moveColumns(true, [athlete], 2, 'toolPanelUi');
-        expect(getPrimaryColumnOrder(toolPanel).slice(0, 3)).toEqual(['athlete', 'age', 'country']);
-        commitChanges(toolPanel);
-        expect(getPrimaryColumnOrder(toolPanel).slice(0, 3)).toEqual(['age', 'athlete', 'country']);
-    });
-
-    test('deferred mode should show a Defer mode toggle in the column tool panel footer', async () => {
-        const { toolPanelGui } = await createDeferredNonPivotGrid();
-
-        expect(toolPanelGui.textContent).toContain('Defer mode');
-        expect(toolPanelGui.textContent).toContain('Apply');
-        expect(toolPanelGui.textContent).toContain('Cancel');
-    });
-
     test('dragging an unchecked column from the column list into row groups in deferred pivot mode stages the pill and checkbox', async () => {
         const { gridApi, toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
 
@@ -1450,71 +1406,6 @@ describe('deferred column tool panel pivot mode', () => {
         expect(dragItem.pivotState.sport?.rowGroup).toBe(false);
     });
 
-    test('turning defer mode back on after leaving pivot mode should keep row groups and values populated', async () => {
-        const { gridApi, toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
-
-        getDeferModeToggle(toolPanelGui).click();
-        getPivotModeToggle(toolPanelGui).click();
-        await waitForNoLoadingRows(gridApi);
-
-        expect(gridApi.isPivotMode()).toBe(false);
-        const liveRowGroupColIds = gridApi.getRowGroupColumns().map((col) => col.getColId());
-        const liveValueColIds = getValueColumnIds(gridApi);
-        expect(liveRowGroupColIds).toEqual(['country', 'sport']);
-        expect(liveValueColIds.length).toBeGreaterThan(0);
-
-        getDeferModeToggle(toolPanelGui).click();
-
-        expect(
-            getUpdateStrategy(toolPanel)
-                .getRowGroupColumns(true)
-                .map((col) => col.getColId())
-        ).toEqual(liveRowGroupColIds);
-        expect(
-            getUpdateStrategy(toolPanel)
-                .getValueColumns(true)
-                .map((col) => col.getColId())
-        ).toEqual(liveValueColIds);
-        expect(toolPanel.rowGroupDropZonePanel.getGui().textContent).toContain('Country');
-        expect(toolPanel.rowGroupDropZonePanel.getGui().textContent).toContain('Sport');
-    });
-
-    test('turning defer mode off then toggling pivot mode should remove and restore the year label immediately', async () => {
-        const { gridApi, toolPanel, toolPanelGui } = await createDeferredPivotModeGrid();
-
-        getDeferModeToggle(toolPanelGui).click();
-        getPivotModeToggle(toolPanelGui).click();
-        await waitForNoLoadingRows(gridApi);
-
-        expect(gridApi.isPivotMode()).toBe(false);
-        let hasYearHeaderGroupText = Array.from(
-            getGridElement(gridApi)!.querySelectorAll('.ag-header-group-text')
-        ).some((el) => el.textContent?.trim() === '2000');
-        expect(hasYearHeaderGroupText).toBe(false);
-
-        getPivotModeToggle(toolPanelGui).click();
-        await waitForNoLoadingRows(gridApi);
-        await asyncSetTimeout(50);
-
-        expect(gridApi.isPivotMode()).toBe(true);
-        expect(gridApi.getPivotColumns().map((col) => col.getColId())).toEqual(['year']);
-        hasYearHeaderGroupText = Array.from(getGridElement(gridApi)!.querySelectorAll('.ag-header-group-text')).some(
-            (el) => el.textContent?.trim() === '2000'
-        );
-        expect(hasYearHeaderGroupText).toBe(true);
-        expect(toolPanel.pivotDropZonePanel.getGui().textContent).toContain('Year');
-    });
-
-    test('turning defer mode off then turning pivot mode off updates the live grid immediately', async () => {
-        const { gridApi, toolPanelGui } = await createDeferredPivotModeGrid();
-
-        getDeferModeToggle(toolPanelGui).click();
-        getPivotModeToggle(toolPanelGui).click();
-        await waitForNoLoadingRows(gridApi);
-
-        expect(gridApi.isPivotMode()).toBe(false);
-    });
-
     test('turning pivot mode back on after disabling and applying restores the previous pivot columns', async () => {
         const { gridApi, toolPanelGui } = await createDeferredPivotModeGrid();
 
@@ -1587,4 +1478,35 @@ describe('deferred column tool panel pivot mode', () => {
 
         expect(toolPanel.primaryColsPanel.primaryColsListPanel.getDisplayedColsList().length).toBeGreaterThan(0);
     });
+
+    test('apply button is disabled when there are no pending changes', async () => {
+        const { toolPanelGui } = await createDeferredNonPivotGrid();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+    });
+
+    test('apply button becomes enabled when pivot mode is toggled off', async () => {
+        const { toolPanelGui } = await createDeferredPivotModeGrid();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+
+        getPivotModeToggle(toolPanelGui).click();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+    });
+
+    test('apply button becomes enabled when a column is toggled and disabled again when toggled back to original state', async () => {
+        const { gridApi, toolPanel, toolPanelGui } = await createDeferredNonPivotGrid();
+        const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+        getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+        getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], true, 'toolPanelUi');
+        toolPanel.refreshDeferredUi();
+
+        expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+    });
 });
diff --git a/testing/behavioural/src/columnToolPanel/deferred-suppress-sync-layout.test.ts b/testing/behavioural/src/columnToolPanel/deferred-suppress-sync-layout.test.ts
new file mode 100644
index 00000000000..3b976265e56
--- /dev/null
+++ b/testing/behavioural/src/columnToolPanel/deferred-suppress-sync-layout.test.ts
@@ -0,0 +1,377 @@
+import type { AgColumn, ColDef, GridApi, IColumnStateUpdateStrategy } from 'ag-grid-community';
+import { setupAgTestIds } from 'ag-grid-community';
+import { AllEnterpriseModule } from 'ag-grid-enterprise';
+
+import { moveItem } from '../../../../packages/ag-grid-enterprise/src/columnToolPanel/columnMoveUtils';
+import { TestGridsManager, asyncSetTimeout } from '../test-utils';
+
+describe('deferred column tool panel with suppressSyncLayoutWithGrid', () => {
+    const gridMgr = new TestGridsManager({
+        modules: [AllEnterpriseModule],
+    });
+
+    const rowData = [
+        { athlete: 'Michael Phelps', age: 23, country: 'United States', sport: 'Swimming', gold: 8 },
+        { athlete: 'Julian Weber', age: 24, country: 'Romania', sport: 'Gymnastics', gold: 2 },
+    ];
+
+    const baseColumnDefs: ColDef[] = [
+        { field: 'athlete' },
+        { field: 'age' },
+        { field: 'country' },
+        { field: 'sport' },
+        { field: 'gold' },
+    ];
+
+    beforeAll(() => {
+        setupAgTestIds();
+    });
+
+    afterEach(() => {
+        gridMgr.reset();
+    });
+
+    async function createGrid(params: { suppressSyncLayoutWithGrid?: boolean; columnDefs?: ColDef[] } = {}): Promise<{
+        gridApi: GridApi;
+        toolPanel: any;
+        toolPanelGui: HTMLElement;
+    }> {
+        const gridApi = await gridMgr.createGridAndWait('myGrid', {
+            columnDefs: params.columnDefs ?? baseColumnDefs,
+            rowData,
+            sideBar: {
+                toolPanels: [
+                    {
+                        id: 'columns',
+                        labelDefault: 'Columns',
+                        labelKey: 'columns',
+                        iconKey: 'columns',
+                        toolPanel: 'agColumnsToolPanel',
+                        toolPanelParams: {
+                            buttons: ['apply', 'cancel'] as const,
+                            suppressSyncLayoutWithGrid: params.suppressSyncLayoutWithGrid ?? true,
+                        },
+                    },
+                ],
+                defaultToolPanel: 'columns',
+            },
+        });
+
+        await asyncSetTimeout(50);
+
+        const toolPanel = gridApi.getToolPanelInstance('columns') as any;
+        return {
+            gridApi,
+            toolPanel,
+            toolPanelGui: toolPanel.getGui(),
+        };
+    }
+
+    function getUpdateStrategy(toolPanel: any): IColumnStateUpdateStrategy {
+        return toolPanel.beans.columnStateUpdateStrategy;
+    }
+
+    function isDeferred(toolPanel: any): boolean {
+        return !!toolPanel['isDeferModeEnabled'];
+    }
+
+    function getApplyButton(toolPanelGui: HTMLElement): HTMLButtonElement {
+        return Array.from(toolPanelGui.querySelectorAll<HTMLButtonElement>('.ag-column-panel-buttons-button')).find(
+            (button) => button.textContent?.trim() === 'Apply'
+        )!;
+    }
+
+    function getCancelButton(toolPanelGui: HTMLElement): HTMLButtonElement {
+        return Array.from(toolPanelGui.querySelectorAll<HTMLButtonElement>('.ag-column-panel-buttons-button')).find(
+            (button) => button.textContent?.trim() === 'Cancel'
+        )!;
+    }
+
+    function getDisplayedPrimaryColumnOrder(toolPanel: any): string[] {
+        return toolPanel.primaryColsPanel.primaryColsListPanel
+            .getDisplayedColsList()
+            .filter((item: any) => !item.group)
+            .map((item: any) => item.column.getColId());
+    }
+
+    function getPrimaryColumnOrder(toolPanel: any): string[] {
+        return toolPanel.beans.colModel.getColDefCols().map((col: any) => col.getColId());
+    }
+
+    async function dragColumnToEnd(toolPanel: any, label: string): Promise<void> {
+        const listPanel = toolPanel.primaryColsPanel.primaryColsListPanel;
+        const virtualList = listPanel['virtualList'];
+        const displayedColsList = listPanel.getDisplayedColsList() as any[];
+        const lastIndex = displayedColsList.length - 1;
+        const movingItem = displayedColsList.find((item: any) => item.displayName === label);
+
+        expect(movingItem).toBeTruthy();
+
+        virtualList.ensureIndexVisible(lastIndex);
+        await asyncSetTimeout(50);
+
+        let component = virtualList.getComponentAt(lastIndex) as any;
+        if (!component) {
+            component = listPanel['createComponentFromItem'](
+                displayedColsList[lastIndex],
+                document.createElement('div')
+            );
+        }
+
+        moveItem(
+            toolPanel.beans,
+            [movingItem.column as AgColumn],
+            {
+                rowIndex: lastIndex,
+                position: 'bottom',
+                component,
+            },
+            {
+                buttons: ['apply', 'cancel'] as const,
+                suppressSyncLayoutWithGrid: true,
+            }
+        );
+        await asyncSetTimeout(50);
+    }
+
+    describe('column reordering', () => {
+        test('allows column reordering in CTP when suppressSyncLayoutWithGrid and deferred mode', async () => {
+            const { toolPanel } = await createGrid({ suppressSyncLayoutWithGrid: true });
+
+            const initialOrder = getDisplayedPrimaryColumnOrder(toolPanel);
+            expect(initialOrder).toEqual(['athlete', 'age', 'country', 'sport', 'gold']);
+
+            await dragColumnToEnd(toolPanel, 'Athlete');
+
+            const newOrder = getDisplayedPrimaryColumnOrder(toolPanel);
+            expect(newOrder).toEqual(['age', 'country', 'sport', 'gold', 'athlete']);
+        });
+
+        test('shows deferred column order after drag and reverts on cancel', async () => {
+            const { toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+
+            await dragColumnToEnd(toolPanel, 'Athlete');
+
+            expect(getDisplayedPrimaryColumnOrder(toolPanel)).toEqual(['age', 'country', 'sport', 'gold', 'athlete']);
+
+            // Grid column order is unchanged
+            expect(getPrimaryColumnOrder(toolPanel)).toEqual(['athlete', 'age', 'country', 'sport', 'gold']);
+
+            getCancelButton(toolPanelGui).click();
+            await asyncSetTimeout(50);
+
+            // Reverts to colDef order
+            expect(getDisplayedPrimaryColumnOrder(toolPanel)).toEqual(['athlete', 'age', 'country', 'sport', 'gold']);
+        });
+
+        test('applies deferred column order on commit', async () => {
+            const { toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+
+            await dragColumnToEnd(toolPanel, 'Athlete');
+
+            expect(getDisplayedPrimaryColumnOrder(toolPanel)).toEqual(['age', 'country', 'sport', 'gold', 'athlete']);
+
+            getApplyButton(toolPanelGui).click();
+            await asyncSetTimeout(50);
+
+            // After apply, colDef order should be updated
+            expect(getPrimaryColumnOrder(toolPanel)).toEqual(['age', 'country', 'sport', 'gold', 'athlete']);
+        });
+    });
+
+    describe('external change resets staged changes', () => {
+        test('external sort change resets staged changes', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+            const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+            // Stage a visibility change
+            getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+            // External sort change
+            gridApi.applyColumnState({ state: [{ colId: 'age', sort: 'asc' }] });
+            await asyncSetTimeout(50);
+
+            // Staged changes should be reset
+            expect(getUpdateStrategy(toolPanel).hasPendingChanges(isDeferred(toolPanel))).toBe(false);
+            expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+        });
+
+        test('external column visibility change resets staged changes', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+            const age = gridApi.getColumn('age')! as AgColumn;
+
+            // Stage a sort change
+            getUpdateStrategy(toolPanel).progressSortFromEvent(true, age, new MouseEvent('click'));
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+            // External visibility change
+            gridApi.setColumnsVisible(['gold'], false);
+            await asyncSetTimeout(50);
+
+            expect(getUpdateStrategy(toolPanel).hasPendingChanges(isDeferred(toolPanel))).toBe(false);
+            expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+        });
+
+        test('external column move does NOT reset staged changes when suppressSyncLayoutWithGrid is true', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+            const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+            // Stage a visibility change
+            getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+            // External column move
+            gridApi.moveColumns(['gold'], 0);
+            await asyncSetTimeout(50);
+
+            // Staged changes should NOT be reset because suppressSyncLayoutWithGrid is true
+            expect(getUpdateStrategy(toolPanel).hasPendingChanges(isDeferred(toolPanel))).toBe(true);
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+        });
+
+        test('external column move DOES reset staged changes when suppressSyncLayoutWithGrid is false', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: false });
+            const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+            // Stage a visibility change
+            getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+            // External column move
+            gridApi.moveColumns(['gold'], 0);
+            await asyncSetTimeout(50);
+
+            // Staged changes SHOULD be reset
+            expect(getUpdateStrategy(toolPanel).hasPendingChanges(isDeferred(toolPanel))).toBe(false);
+            expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+        });
+
+        test('external newColumnsLoaded resets staged changes', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+            const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+            getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+            // setColumnDefs triggers newColumnsLoaded
+            gridApi.setGridOption('columnDefs', baseColumnDefs);
+            await asyncSetTimeout(50);
+
+            expect(getUpdateStrategy(toolPanel).hasPendingChanges(isDeferred(toolPanel))).toBe(false);
+            expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+        });
+
+        test('commit does not trigger external reset (isCommitting guard)', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: false });
+            const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+            // Stage a visibility change
+            getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+            expect(gridApi.getColumn('athlete')!.isVisible()).toBe(true);
+
+            // Apply commits staged changes — this fires grid events, but should NOT reset
+            getApplyButton(toolPanelGui).click();
+            await asyncSetTimeout(50);
+
+            // The change should have been applied
+            expect(gridApi.getColumn('athlete')!.isVisible()).toBe(false);
+            // Apply button should now be disabled (no pending changes)
+            expect(getApplyButton(toolPanelGui).disabled).toBe(true);
+        });
+
+        test('pinning does not reset staged changes', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+            const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+            getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+            // External pinning
+            gridApi.setColumnsPinned(['age'], 'left');
+            await asyncSetTimeout(50);
+
+            // Staged changes should NOT be reset
+            expect(getUpdateStrategy(toolPanel).hasPendingChanges(isDeferred(toolPanel))).toBe(true);
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+        });
+
+        test('resizing does not reset staged changes', async () => {
+            const { gridApi, toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+            const athlete = gridApi.getColumn('athlete')! as AgColumn;
+
+            getUpdateStrategy(toolPanel).setColumnsVisible(true, [athlete], false, 'toolPanelUi');
+            toolPanel.refreshDeferredUi();
+
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+
+            // External resize
+            gridApi.setColumnWidths([{ key: 'age', newWidth: 200 }]);
+            await asyncSetTimeout(50);
+
+            // Staged changes should NOT be reset
+            expect(getUpdateStrategy(toolPanel).hasPendingChanges(isDeferred(toolPanel))).toBe(true);
+            expect(getApplyButton(toolPanelGui).disabled).toBe(false);
+        });
+
+        test('no reset if no pending changes', async () => {
+            const { gridApi, toolPanel } = await createGrid({ suppressSyncLayoutWithGrid: true });
+            const resetSpy = vi.spyOn(getUpdateStrategy(toolPanel), 'reset');
+
+            // External sort change with no pending changes
+            gridApi.applyColumnState({ state: [{ colId: 'age', sort: 'asc' }] });
+            await asyncSetTimeout(50);
+
+            // reset should not have been called since there were no pending changes
+            expect(resetSpy).not.toHaveBeenCalled();
+        });
+    });
+
+    describe('initial state and fallback', () => {
+        test('initial render shows colDef order when suppressSyncLayoutWithGrid is true', async () => {
+            const { toolPanel } = await createGrid({ suppressSyncLayoutWithGrid: true });
+
+            expect(getDisplayedPrimaryColumnOrder(toolPanel)).toEqual(['athlete', 'age', 'country', 'sport', 'gold']);
+        });
+
+        test('after cancel, CTP reverts to colDef order', async () => {
+            const { toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+
+            await dragColumnToEnd(toolPanel, 'Athlete');
+            expect(getDisplayedPrimaryColumnOrder(toolPanel)).toEqual(['age', 'country', 'sport', 'gold', 'athlete']);
+
+            getCancelButton(toolPanelGui).click();
+            await asyncSetTimeout(50);
+
+            expect(getDisplayedPrimaryColumnOrder(toolPanel)).toEqual(['athlete', 'age', 'country', 'sport', 'gold']);
+        });
+
+        test('after apply, CTP shows colDef order matching applied state', async () => {
+            const { toolPanel, toolPanelGui } = await createGrid({ suppressSyncLayoutWithGrid: true });
+
+            await dragColumnToEnd(toolPanel, 'Athlete');
+
+            getApplyButton(toolPanelGui).click();
+            await asyncSetTimeout(50);
+
+            // After apply, the colDef order was updated by commit, and the CTP shows it
+            expect(getDisplayedPrimaryColumnOrder(toolPanel)).toEqual(['age', 'country', 'sport', 'gold', 'athlete']);
+            expect(getPrimaryColumnOrder(toolPanel)).toEqual(['age', 'country', 'sport', 'gold', 'athlete']);
+        });
+    });
+});
diff --git a/testing/behavioural/src/columnToolPanel/tool-panel-context-menu.test.ts b/testing/behavioural/src/columnToolPanel/tool-panel-context-menu.test.ts
index 395b3c80625..d5a8e96dff0 100644
--- a/testing/behavioural/src/columnToolPanel/tool-panel-context-menu.test.ts
+++ b/testing/behavioural/src/columnToolPanel/tool-panel-context-menu.test.ts
@@ -182,15 +182,12 @@ describe('Cell Editing Start', async () => {
             return panel.getGui().textContent ?? '';
         }
 
-        function createDeferredContextMenu(
-            gridApi: GridApi,
-            gridDiv: HTMLElement,
-            columnId: string,
-            params: { deferApply: boolean } = { deferApply: true }
-        ): any {
+        const DEFERRED_PARAMS = { buttons: ['apply', 'cancel'] as const };
+
+        function createDeferredContextMenu(gridApi: GridApi, gridDiv: HTMLElement, columnId: string): any {
             const column = gridApi.getColumn(columnId)! as AgColumn;
             return column.createBean(
-                new ToolPanelContextMenu(column as any, new MouseEvent('contextmenu'), gridDiv, params)
+                new ToolPanelContextMenu(column as any, new MouseEvent('contextmenu'), gridDiv, DEFERRED_PARAMS)
             );
         }
 
@@ -211,7 +208,7 @@ describe('Cell Editing Start', async () => {
                             labelKey: 'columns',
                             iconKey: 'columns',
                             toolPanel: 'agColumnsToolPanel',
-                            toolPanelParams: { deferApply: true },
+                            toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                         },
                     ],
                     defaultToolPanel: 'columns',
@@ -247,7 +244,7 @@ describe('Cell Editing Start', async () => {
                             labelKey: 'columns',
                             iconKey: 'columns',
                             toolPanel: 'agColumnsToolPanel',
-                            toolPanelParams: { deferApply: true },
+                            toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                         },
                     ],
                     defaultToolPanel: 'columns',
@@ -287,7 +284,7 @@ describe('Cell Editing Start', async () => {
                             labelKey: 'columns',
                             iconKey: 'columns',
                             toolPanel: 'agColumnsToolPanel',
-                            toolPanelParams: { deferApply: true },
+                            toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                         },
                     ],
                     defaultToolPanel: 'columns',
@@ -323,7 +320,7 @@ describe('Cell Editing Start', async () => {
                             labelKey: 'columns',
                             iconKey: 'columns',
                             toolPanel: 'agColumnsToolPanel',
-                            toolPanelParams: { deferApply: true },
+                            toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                         },
                     ],
                     defaultToolPanel: 'columns',
@@ -364,7 +361,7 @@ describe('Cell Editing Start', async () => {
                             labelKey: 'columns',
                             iconKey: 'columns',
                             toolPanel: 'agColumnsToolPanel',
-                            toolPanelParams: { deferApply: true },
+                            toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                         },
                     ],
                     defaultToolPanel: 'columns',
@@ -404,7 +401,7 @@ describe('Cell Editing Start', async () => {
                             labelKey: 'columns',
                             iconKey: 'columns',
                             toolPanel: 'agColumnsToolPanel',
-                            toolPanelParams: { deferApply: true },
+                            toolPanelParams: { buttons: ['apply', 'cancel'] as const },
                         },
                     ],
                     defaultToolPanel: 'columns',
diff --git a/testing/behavioural/src/columns/cell-data-type-object-validation.test.ts b/testing/behavioural/src/columns/cell-data-type-object-validation.test.ts
new file mode 100644
index 00000000000..0f374893d49
--- /dev/null
+++ b/testing/behavioural/src/columns/cell-data-type-object-validation.test.ts
@@ -0,0 +1,147 @@
+import { afterEach, beforeEach, describe, expect, test } from 'vitest';
+
+import { ClientSideRowModelModule, TextEditorModule } from 'ag-grid-community';
+
+import { TestGridsManager } from '../test-utils';
+
+interface RowData {
+    id: string;
+    value: { label: string };
+}
+
+const ROW_DATA: RowData[] = [
+    { id: '1', value: { label: 'Alpha' } },
+    { id: '2', value: { label: 'Beta' } },
+];
+
+// When a column's data type is inferred as 'object', the grid warns if no Value Formatter is provided
+// (needed to display the value) or no Value Parser is provided on editable columns (needed to parse
+// edited values back to objects).
+//
+// The hint to also set `cellDataType: 'object'` explicitly is only included in the Value Parser
+// warning, because setting a `valueParser` blocks inference — meaning the grid will no longer infer
+// 'object' automatically and the type must be declared. Setting a `valueFormatter` does not block
+// inference, so no explicit `cellDataType` is needed in that case.
+describe('object cellDataType validation warnings', () => {
+    const gridsManager = new TestGridsManager({ modules: [ClientSideRowModelModule, TextEditorModule] });
+
+    let warnSpy: ReturnType<typeof vitest.spyOn>;
+
+    beforeEach(() => {
+        gridsManager.reset();
+        warnSpy = vitest.spyOn(console, 'warn').mockImplementation(() => {});
+    });
+
+    afterEach(() => {
+        warnSpy.mockRestore();
+        gridsManager.reset();
+    });
+
+    test('warns about missing valueFormatter when cellDataType=object is inferred', () => {
+        gridsManager.createGrid('grid', {
+            columnDefs: [{ field: 'value' }],
+            rowData: ROW_DATA,
+        });
+
+        expect(warnSpy).toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining(
+                'Cell data type is "object" (inferred) but no Value Formatter has been provided for column "value"'
+            ),
+            expect.any(String)
+        );
+        expect(warnSpy).not.toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('  - "colDef.cellDataType = \'object\'"'),
+            expect.any(String)
+        );
+    });
+
+    test('warns about missing valueFormatter when cellDataType=object is set explicitly on colDef - no inference hint', () => {
+        gridsManager.createGrid('grid', {
+            columnDefs: [{ field: 'value', cellDataType: 'object' }],
+            rowData: ROW_DATA,
+        });
+
+        expect(warnSpy).toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('Cell data type is "object" but no Value Formatter has been provided'),
+            expect.any(String)
+        );
+        expect(warnSpy).not.toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('(inferred)'),
+            expect.any(String)
+        );
+    });
+
+    test('warns about missing valueFormatter when cellDataType=object is set on defaultColDef - no inference hint', () => {
+        gridsManager.createGrid('grid', {
+            defaultColDef: { cellDataType: 'object' },
+            columnDefs: [{ field: 'value' }],
+            rowData: ROW_DATA,
+        });
+
+        expect(warnSpy).toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('Cell data type is "object" but no Value Formatter has been provided'),
+            expect.any(String)
+        );
+        expect(warnSpy).not.toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('(inferred)'),
+            expect.any(String)
+        );
+    });
+
+    test('warns about missing valueParser when cellDataType=object is inferred and column is editable, includes hint to set explicitly', () => {
+        gridsManager.createGrid('grid', {
+            columnDefs: [{ field: 'value', editable: true }],
+            rowData: ROW_DATA,
+        });
+
+        expect(warnSpy).toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('Cell data type is "object" (inferred) but no Value Parser has been provided'),
+            expect.any(String)
+        );
+        expect(warnSpy).toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('  - "colDef.cellDataType = \'object\'"'),
+            expect.any(String)
+        );
+    });
+
+    test('no warning when a custom valueFormatter is provided for inferred object type', () => {
+        gridsManager.createGrid('grid', {
+            columnDefs: [{ field: 'value', valueFormatter: (p) => p.value?.label ?? '' }],
+            rowData: ROW_DATA,
+        });
+
+        expect(warnSpy).not.toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('Cell data type is "object"'),
+            expect.any(String)
+        );
+    });
+
+    test('no warning when valueFormatter and valueParser are both provided for editable inferred object type', () => {
+        gridsManager.createGrid('grid', {
+            columnDefs: [
+                {
+                    field: 'value',
+                    editable: true,
+                    valueFormatter: (p) => p.value?.label ?? '',
+                    valueParser: (p) => ({ label: p.newValue }),
+                },
+            ],
+            rowData: ROW_DATA,
+        });
+
+        expect(warnSpy).not.toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('Cell data type is "object"'),
+            expect.any(String)
+        );
+    });
+});
diff --git a/testing/behavioural/src/filters/set-filter-complex-objects.test.ts b/testing/behavioural/src/filters/set-filter-complex-objects.test.ts
new file mode 100644
index 00000000000..7d84d751473
--- /dev/null
+++ b/testing/behavioural/src/filters/set-filter-complex-objects.test.ts
@@ -0,0 +1,507 @@
+import type { GridApi, GridOptions, ISetFilterParams, KeyCreatorParams, ValueFormatterParams } from 'ag-grid-community';
+import { ClientSideRowModelModule, TextEditorModule } from 'ag-grid-community';
+import { SetFilterModule } from 'ag-grid-enterprise';
+import type { SetFilter } from 'ag-grid-enterprise';
+
+import { TestGridsManager, asyncSetTimeout } from '../test-utils';
+
+interface Country {
+    name: string;
+    code: string;
+}
+
+interface RowData {
+    athlete: string;
+    country: Country;
+}
+
+const countryKeyCreator = (params: KeyCreatorParams): string => {
+    return params.value.code;
+};
+
+const countryValueFormatter = (params: ValueFormatterParams): string => {
+    return params.value.name;
+};
+
+const ROW_DATA: RowData[] = [
+    { athlete: 'Michael Phelps', country: { name: 'United States', code: 'US' } },
+    { athlete: 'Usain Bolt', country: { name: 'Jamaica', code: 'JM' } },
+    { athlete: 'Mo Farah', country: { name: 'Great Britain', code: 'GB' } },
+    { athlete: 'Allyson Felix', country: { name: 'United States', code: 'US' } },
+    { athlete: 'Shelly-Ann Fraser-Pryce', country: { name: 'Jamaica', code: 'JM' } },
+    { athlete: 'Greg Rutherford', country: { name: 'Great Britain', code: 'GB' } },
+    { athlete: 'Wayde van Niekerk', country: { name: 'South Africa', code: 'ZA' } },
+];
+
+describe('Set Filter Complex Objects', () => {
+    const gridsManager = new TestGridsManager({
+        modules: [ClientSideRowModelModule, SetFilterModule, TextEditorModule],
+    });
+
+    afterEach(() => gridsManager.reset());
+
+    async function createGridWithComplexObjects(overrides?: Partial<GridOptions<RowData>>): Promise<GridApi<RowData>> {
+        return gridsManager.createGridAndWait('grid1', {
+            columnDefs: [
+                {
+                    field: 'athlete',
+                },
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    keyCreator: countryKeyCreator,
+                    valueFormatter: countryValueFormatter,
+                    filterParams: {
+                        valueFormatter: countryValueFormatter,
+                        keyCreator: countryKeyCreator,
+                    } as ISetFilterParams,
+                },
+            ],
+            rowData: ROW_DATA,
+            ...overrides,
+        });
+    }
+
+    async function getSetFilter(api: GridApi): Promise<SetFilter<any>> {
+        const filter = (await api.getColumnFilterInstance('country')) as SetFilter<any> | null | undefined;
+        if (!filter) {
+            throw new Error('Expected SetFilter instance for country column');
+        }
+        return filter;
+    }
+
+    test('filter keys are created from keyCreator', async () => {
+        const api = await createGridWithComplexObjects();
+        const setFilter = await getSetFilter(api);
+
+        const keys = (await setFilter.handler.valueModel.allKeys) ?? [];
+        expect([...keys].sort()).toEqual(['GB', 'JM', 'US', 'ZA']);
+    });
+
+    test('filtering with complex object values filters rows correctly', async () => {
+        const api = await createGridWithComplexObjects();
+
+        await api.setColumnFilterModel('country', { filterType: 'set', values: ['US'] });
+        api.onFilterChanged();
+        await asyncSetTimeout(0);
+
+        const displayedRows: string[] = [];
+        api.forEachNodeAfterFilter((node) => {
+            if (node.data) {
+                displayedRows.push(node.data.athlete);
+            }
+        });
+
+        expect(displayedRows.sort()).toEqual(['Allyson Felix', 'Michael Phelps']);
+    });
+
+    test('filtering with multiple complex object keys', async () => {
+        const api = await createGridWithComplexObjects();
+
+        await api.setColumnFilterModel('country', { filterType: 'set', values: ['US', 'JM'] });
+        api.onFilterChanged();
+        await asyncSetTimeout(0);
+
+        const displayedRows: string[] = [];
+        api.forEachNodeAfterFilter((node) => {
+            if (node.data) {
+                displayedRows.push(node.data.athlete);
+            }
+        });
+
+        expect(displayedRows.sort()).toEqual([
+            'Allyson Felix',
+            'Michael Phelps',
+            'Shelly-Ann Fraser-Pryce',
+            'Usain Bolt',
+        ]);
+    });
+
+    test('getModelAsString returns formatted values for complex objects', async () => {
+        const api = await createGridWithComplexObjects();
+        const setFilter = await getSetFilter(api);
+
+        await api.setColumnFilterModel('country', { filterType: 'set', values: ['US', 'GB'] });
+        api.onFilterChanged();
+        await asyncSetTimeout(0);
+
+        const model = api.getFilterModel().country;
+        const modelAsString = setFilter.getModelAsString(model);
+
+        expect(modelAsString).toContain('United States');
+        expect(modelAsString).toContain('Great Britain');
+    });
+
+    test('set filter list items display formatted values, not [object Object]', async () => {
+        const api = await createGridWithComplexObjects();
+        const setFilter = await getSetFilter(api);
+
+        const allKeys = (await setFilter.handler.valueModel.allKeys) ?? [];
+        for (const key of allKeys) {
+            const value = setFilter.handler.valueModel.allValues.get(key);
+            // The value stored should be the complex object, not a string
+            expect(value).toBeDefined();
+            expect(typeof value).toBe('object');
+        }
+    });
+
+    test('mini filter searches against formatted values for complex objects', async () => {
+        const api = await createGridWithComplexObjects();
+        const setFilter = await getSetFilter(api);
+
+        // Open the filter to initialise the mini filter
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        setFilter.setMiniFilter('United');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual(['US']);
+    });
+
+    test('mini filter with no valueFormatter on filterParams falls back to column valueFormatter', async () => {
+        const api = await gridsManager.createGridAndWait('grid2', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    keyCreator: countryKeyCreator,
+                    valueFormatter: countryValueFormatter,
+                    filterParams: {
+                        keyCreator: countryKeyCreator,
+                        // No valueFormatter on filterParams — should use column's valueFormatter
+                    } as ISetFilterParams,
+                },
+            ],
+            rowData: ROW_DATA,
+        });
+
+        const setFilter = await getSetFilter(api);
+
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        setFilter.setMiniFilter('Jamaica');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual(['JM']);
+    });
+
+    test('mini filter with cellDataType false does not pass colDef valueFormatter to filter', async () => {
+        vi.spyOn(console, 'warn').mockImplementation(() => {});
+        vi.spyOn(console, 'error').mockImplementation(() => {});
+
+        const api = await gridsManager.createGridAndWait('grid4', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    valueFormatter: countryValueFormatter,
+                    cellDataType: false,
+                    filterParams: {
+                        keyCreator: countryKeyCreator,
+                        // No valueFormatter on filterParams, and cellDataType: false prevents auto-propagation
+                    } as ISetFilterParams,
+                },
+            ],
+            rowData: ROW_DATA,
+        });
+
+        const setFilter = await getSetFilter(api);
+
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        // With cellDataType: false, the colDef valueFormatter is not passed to the filter,
+        // so mini filter cannot match against formatted names
+        setFilter.setMiniFilter('Jamaica');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual([]);
+
+        vi.restoreAllMocks();
+    });
+
+    test('mini filter with explicit cellDataType object uses colDef valueFormatter in filter', async () => {
+        const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+        const api = await gridsManager.createGridAndWait('grid5', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    cellDataType: 'object',
+                    filterParams: {
+                        keyCreator: countryKeyCreator,
+                        // No valueFormatter on filterParams — should use the one from dataTypeService
+                    } as ISetFilterParams,
+                },
+            ],
+            dataTypeDefinitions: {
+                object: {
+                    baseDataType: 'object',
+                    extendsDataType: 'object',
+                    valueFormatter: countryValueFormatter,
+                },
+            },
+            rowData: ROW_DATA,
+        });
+
+        const setFilter = await getSetFilter(api);
+
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        // Mini filter should search against the formatted country name
+        setFilter.setMiniFilter('Jamaica');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual(['JM']);
+
+        warnSpy.mockRestore();
+    });
+
+    test('mini filter with inferred cellDataType object uses colDef valueFormatter in filter', async () => {
+        const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+        const api = await gridsManager.createGridAndWait('grid6', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    // cellDataType will be inferred as 'object' from the complex rowData
+                    filterParams: {
+                        keyCreator: countryKeyCreator,
+                        // No valueFormatter on filterParams — should use the one inferred via dataTypeService
+                    } as ISetFilterParams,
+                },
+            ],
+            dataTypeDefinitions: {
+                object: {
+                    baseDataType: 'object',
+                    extendsDataType: 'object',
+                    valueFormatter: countryValueFormatter,
+                },
+            },
+            rowData: ROW_DATA,
+        });
+
+        const setFilter = await getSetFilter(api);
+
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        // Mini filter should search against the formatted country name
+        setFilter.setMiniFilter('Jamaica');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual(['JM']);
+
+        warnSpy.mockRestore();
+    });
+
+    test('clearing filter restores all rows', async () => {
+        const api = await createGridWithComplexObjects();
+
+        await api.setColumnFilterModel('country', { filterType: 'set', values: ['US'] });
+        api.onFilterChanged();
+        await asyncSetTimeout(0);
+
+        await api.setColumnFilterModel('country', null);
+        api.onFilterChanged();
+        await asyncSetTimeout(0);
+
+        let rowCount = 0;
+        api.forEachNodeAfterFilter((node) => {
+            if (node.data) {
+                rowCount++;
+            }
+        });
+        expect(rowCount).toBe(ROW_DATA.length);
+    });
+
+    test('getFilterModel returns keys, not objects', async () => {
+        const api = await createGridWithComplexObjects();
+
+        await api.setColumnFilterModel('country', { filterType: 'set', values: ['US', 'ZA'] });
+        api.onFilterChanged();
+        await asyncSetTimeout(0);
+
+        const model = api.getFilterModel();
+        expect(model.country).toEqual({
+            filterType: 'set',
+            values: ['US', 'ZA'],
+        });
+    });
+
+    test('editable column with valueParser prevents cellDataType object inference so valueFormatter is not applied', async () => {
+        vi.spyOn(console, 'warn').mockImplementation(() => {});
+        vi.spyOn(console, 'error').mockImplementation(() => {});
+
+        const api = await gridsManager.createGridAndWait('grid7', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    editable: true,
+                    // valueParser on colDef prevents cellDataType inference — 'object' won't be inferred,
+                    // so the dataTypeDefinitions.object.valueFormatter won't be applied to this column
+                    valueParser: (params) => ({ name: params.newValue, code: params.newValue?.substring(0, 2) }),
+                    filterParams: {
+                        keyCreator: countryKeyCreator,
+                    } as ISetFilterParams,
+                },
+            ],
+            dataTypeDefinitions: {
+                object: {
+                    baseDataType: 'object',
+                    extendsDataType: 'object',
+                    valueFormatter: countryValueFormatter,
+                    valueParser: (params) => ({ name: params.newValue, code: params.newValue?.substring(0, 2) }),
+                },
+            },
+            rowData: ROW_DATA,
+        });
+
+        const setFilter = await getSetFilter(api);
+
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        // valueParser on colDef prevented cellDataType:'object' inference, so the object
+        // dataTypeDefinition's valueFormatter was never applied to this column.
+        // The mini filter cannot match against formatted country names.
+        setFilter.setMiniFilter('Jamaica');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual([]);
+
+        vi.restoreAllMocks();
+    });
+
+    test('editable column with explicit cellDataType object and valueParser still applies valueFormatter', async () => {
+        vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+        const api = await gridsManager.createGridAndWait('grid8', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    editable: true,
+                    // Explicit cellDataType bypasses inference, so valueParser doesn't prevent it
+                    cellDataType: 'object',
+                    filterParams: {
+                        keyCreator: countryKeyCreator,
+                    } as ISetFilterParams,
+                },
+            ],
+            dataTypeDefinitions: {
+                object: {
+                    baseDataType: 'object',
+                    extendsDataType: 'object',
+                    valueFormatter: countryValueFormatter,
+                    valueParser: (params) => ({ name: params.newValue, code: params.newValue?.substring(0, 2) }),
+                },
+            },
+            rowData: ROW_DATA,
+        });
+
+        const setFilter = await getSetFilter(api);
+
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        // Explicit cellDataType:'object' means the dataTypeDefinition's valueFormatter is applied
+        // regardless of whether a valueParser is on the colDef
+        setFilter.setMiniFilter('Jamaica');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual(['JM']);
+
+        vi.restoreAllMocks();
+    });
+
+    test('editable column without valueParser on colDef infers cellDataType object and applies valueFormatter', async () => {
+        vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+        const api = await gridsManager.createGridAndWait('grid9', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    editable: true,
+                    // No valueParser on colDef — cellDataType:'object' will be inferred from complex rowData,
+                    // and the dataTypeDefinition's valueFormatter + valueParser will both be applied
+                    filterParams: {
+                        keyCreator: countryKeyCreator,
+                    } as ISetFilterParams,
+                },
+            ],
+            dataTypeDefinitions: {
+                object: {
+                    baseDataType: 'object',
+                    extendsDataType: 'object',
+                    valueFormatter: countryValueFormatter,
+                    valueParser: (params) => ({ name: params.newValue, code: params.newValue?.substring(0, 2) }),
+                },
+            },
+            rowData: ROW_DATA,
+        });
+
+        const setFilter = await getSetFilter(api);
+
+        api.showColumnFilter('country');
+        await asyncSetTimeout(0);
+
+        // No valueParser on colDef, so inference proceeds — valueFormatter is applied
+        setFilter.setMiniFilter('Jamaica');
+        await asyncSetTimeout(0);
+
+        const displayedKeys = (setFilter as any).displayValueModel.getDisplayedKeys();
+        expect(displayedKeys).toEqual(['JM']);
+
+        vi.restoreAllMocks();
+    });
+
+    test('complex objects with no keyCreator or valueFormatter warns and does not show [object Object] in filter list', async () => {
+        const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+        const api = await gridsManager.createGridAndWait('grid3', {
+            columnDefs: [
+                {
+                    field: 'country',
+                    filter: 'agSetColumnFilter',
+                    // No keyCreator, no valueFormatter — complex objects will serialise to [object Object]
+                },
+            ],
+            rowData: ROW_DATA,
+        });
+
+        await asyncSetTimeout(0);
+
+        // Warning #48: cellDataType 'object' inferred but no valueFormatter provided
+        expect(warnSpy).toHaveBeenCalledWith(
+            expect.any(String),
+            expect.stringContaining('Cell data type is "object" (inferred) but no Value Formatter has been provided'),
+            expect.any(String)
+        );
+
+        const setFilter = (await api.getColumnFilterInstance('country')) as SetFilter<any>;
+        const allKeys = (await setFilter!.handler.valueModel.allKeys) ?? [];
+
+        // Without keyCreator, all complex objects collapse to a single '[object Object]' key
+        const keys = [...allKeys];
+        for (const key of keys) {
+            expect(key).toBe('[object Object]');
+        }
+
+        warnSpy.mockRestore();
+    });
+});
