This is an automated email from the ASF dual-hosted git repository.

kgabryje pushed a commit to branch what-if
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 106baa67e5eb960c6ffe20171271cf685de6c42a
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Fri Dec 19 10:03:35 2025 +0100

    Saving simulations
---
 .../src/components/Icons/AntdEnhanced.tsx          |   2 +
 .../WhatIfDrawer/SaveSimulationModal.tsx           | 170 +++++++++
 .../components/WhatIfDrawer/WhatIfAIInsights.tsx   |  17 +
 .../components/WhatIfDrawer/WhatIfHeaderMenu.tsx   | 252 ++++++++++++++
 .../dashboard/components/WhatIfDrawer/index.tsx    | 108 +++++-
 .../dashboard/components/WhatIfDrawer/styles.ts    |   6 +
 .../components/WhatIfDrawer/useWhatIfApply.ts      |  80 ++++-
 .../dashboard/components/WhatIfDrawer/whatIfApi.ts | 189 ++++++++++
 .../src/pages/WhatIfSimulationList/index.tsx       | 385 +++++++++++++++++++++
 superset-frontend/src/views/routes.tsx             |  11 +
 superset/daos/what_if_simulation.py                | 106 ++++++
 ...9_10-00_b8f3a2c9d1e5_add_what_if_simulations.py | 116 +++++++
 superset/what_if/api.py                            | 288 ++++++++++++++-
 superset/what_if/commands/interpret.py             |   2 +-
 superset/what_if/commands/simulation_create.py     |  68 ++++
 superset/what_if/commands/simulation_delete.py     |  60 ++++
 superset/what_if/commands/simulation_update.py     |  82 +++++
 superset/what_if/commands/suggest_related.py       |   2 +-
 superset/what_if/exceptions.py                     |  62 ++++
 superset/what_if/models.py                         |  85 +++++
 superset/what_if/schemas.py                        |  79 +++++
 21 files changed, 2161 insertions(+), 9 deletions(-)

diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
 
b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
index 8ff6302d3b..012777f5f7 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx
@@ -77,6 +77,7 @@ import {
   FileOutlined,
   FileTextOutlined,
   FireOutlined,
+  FolderOpenOutlined,
   FormOutlined,
   FullscreenExitOutlined,
   FullscreenOutlined,
@@ -217,6 +218,7 @@ const AntdIcons = {
   FileOutlined,
   FileTextOutlined,
   FireOutlined,
+  FolderOpenOutlined,
   FormOutlined,
   FullscreenExitOutlined,
   FullscreenOutlined,
diff --git 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/SaveSimulationModal.tsx
 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/SaveSimulationModal.tsx
new file mode 100644
index 0000000000..da66a1b83d
--- /dev/null
+++ 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/SaveSimulationModal.tsx
@@ -0,0 +1,170 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useCallback, useEffect } from 'react';
+import { t } from '@superset-ui/core';
+import { styled } from '@apache-superset/core/ui';
+import { Form, Input } from '@superset-ui/core/components';
+import { StandardModal } from 'src/components/Modal';
+import { WhatIfModification } from './types';
+import {
+  createSimulation,
+  updateSimulation,
+  WhatIfSimulation,
+} from './whatIfApi';
+
+const ModalContent = styled.div`
+  padding: ${({ theme }) => theme.sizeUnit * 4}px;
+
+  .ant-form-item {
+    margin-bottom: ${({ theme }) => theme.sizeUnit * 6}px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .ant-form-item-label {
+    padding-bottom: ${({ theme }) => theme.sizeUnit}px;
+  }
+`;
+
+const { TextArea } = Input;
+
+interface SaveSimulationModalProps {
+  show: boolean;
+  onHide: () => void;
+  onSaved: (simulation: WhatIfSimulation) => void;
+  dashboardId: number;
+  modifications: WhatIfModification[];
+  cascadingEffectsEnabled: boolean;
+  existingSimulation?: WhatIfSimulation | null;
+  addSuccessToast: (msg: string) => void;
+  addDangerToast: (msg: string) => void;
+}
+
+const SaveSimulationModal = ({
+  show,
+  onHide,
+  onSaved,
+  dashboardId,
+  modifications,
+  cascadingEffectsEnabled,
+  existingSimulation,
+  addSuccessToast,
+  addDangerToast,
+}: SaveSimulationModalProps) => {
+  const [form] = Form.useForm();
+
+  const isUpdate = Boolean(existingSimulation);
+
+  useEffect(() => {
+    if (show) {
+      form.setFieldsValue({
+        name: existingSimulation?.name || '',
+        description: existingSimulation?.description || '',
+      });
+    }
+  }, [show, existingSimulation, form]);
+
+  const handleSave = useCallback(async () => {
+    try {
+      const values = await form.validateFields();
+
+      if (isUpdate && existingSimulation) {
+        await updateSimulation(existingSimulation.id, {
+          name: values.name,
+          description: values.description,
+          modifications,
+          cascadingEffectsEnabled,
+        });
+        const updatedSimulation: WhatIfSimulation = {
+          ...existingSimulation,
+          name: values.name,
+          description: values.description,
+          modifications,
+          cascadingEffectsEnabled,
+        };
+        onSaved(updatedSimulation);
+        addSuccessToast(t('Simulation updated successfully'));
+      } else {
+        const simulation = await createSimulation({
+          name: values.name,
+          description: values.description,
+          dashboardId,
+          modifications,
+          cascadingEffectsEnabled,
+        });
+        onSaved(simulation);
+        addSuccessToast(t('Simulation saved successfully'));
+      }
+
+      onHide();
+    } catch (error) {
+      addDangerToast(t('Failed to save simulation'));
+    }
+  }, [
+    form,
+    isUpdate,
+    existingSimulation,
+    modifications,
+    cascadingEffectsEnabled,
+    dashboardId,
+    onSaved,
+    onHide,
+    addSuccessToast,
+    addDangerToast,
+  ]);
+
+  const handleCancel = useCallback(() => {
+    form.resetFields();
+    onHide();
+  }, [form, onHide]);
+
+  return (
+    <StandardModal
+      show={show}
+      onHide={handleCancel}
+      onSave={handleSave}
+      title={isUpdate ? t('Update Simulation') : t('Save Simulation')}
+      width={500}
+      saveText={isUpdate ? t('Update') : t('Save')}
+    >
+      <ModalContent>
+        <Form form={form} layout="vertical">
+          <Form.Item
+            name="name"
+            label={t('Name')}
+            rules={[{ required: true, message: t('Please enter a name') }]}
+          >
+            <Input placeholder={t('My What-If Scenario')} />
+          </Form.Item>
+          <Form.Item name="description" label={t('Description')}>
+            <TextArea
+              placeholder={t('Optional description of this simulation')}
+              rows={3}
+            />
+          </Form.Item>
+        </Form>
+      </ModalContent>
+    </StandardModal>
+  );
+};
+
+export default SaveSimulationModal;
diff --git 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx
index 8778017172..db1fe6c44e 100644
--- 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx
+++ 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfAIInsights.tsx
@@ -188,11 +188,14 @@ const CollapsePanelHeader = styled.div<{ insightType: 
WhatIfInsightType }>`
 interface WhatIfAIInsightsProps {
   affectedChartIds: number[];
   modifications: WhatIfModification[];
+  /** Ref to register the abort function for external control */
+  abortRef?: React.MutableRefObject<(() => void) | null>;
 }
 
 const WhatIfAIInsights = ({
   affectedChartIds,
   modifications,
+  abortRef,
 }: WhatIfAIInsightsProps) => {
   const [status, setStatus] = useState<WhatIfAIStatus>('idle');
   const [response, setResponse] = useState<WhatIfInterpretResponse | null>(
@@ -219,6 +222,20 @@ const WhatIfAIInsights = ({
     [],
   );
 
+  // Register abort function with external ref for parent control
+  useEffect(() => {
+    if (abortRef) {
+      abortRef.current = () => {
+        abortControllerRef.current?.abort();
+      };
+    }
+    return () => {
+      if (abortRef) {
+        abortRef.current = null;
+      }
+    };
+  }, [abortRef]);
+
   // Track modification changes to reset status when user adjusts the slider
   const modificationsKey = getModificationsKey(modifications);
   const prevModificationsKeyRef = useRef<string>(modificationsKey);
diff --git 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfHeaderMenu.tsx 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfHeaderMenu.tsx
new file mode 100644
index 0000000000..1b0a7d5de7
--- /dev/null
+++ 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/WhatIfHeaderMenu.tsx
@@ -0,0 +1,252 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useState, useEffect, useCallback, Key } from 'react';
+import { t } from '@superset-ui/core';
+import { css, useTheme } from '@apache-superset/core/ui';
+import { Menu } from '@superset-ui/core/components/Menu';
+import {
+  NoAnimationDropdown,
+  Button,
+  Tooltip,
+} from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import { Link } from 'react-router-dom';
+import { fetchSimulations, WhatIfSimulation } from './whatIfApi';
+
+enum MenuKeys {
+  LoadSimulation = 'load-simulation',
+  SaveSimulation = 'save-simulation',
+  SaveAsNew = 'save-as-new',
+  ManageSimulations = 'manage-simulations',
+}
+
+interface WhatIfHeaderMenuProps {
+  dashboardId: number;
+  selectedSimulation: WhatIfSimulation | null;
+  onSelectSimulation: (simulation: WhatIfSimulation | null) => void;
+  onSaveClick: () => void;
+  onSaveAsNewClick: () => void;
+  hasModifications: boolean;
+  refreshTrigger?: number;
+  addDangerToast: (msg: string) => void;
+}
+
+const VerticalDotsTrigger = () => {
+  const theme = useTheme();
+  return (
+    <Icons.EllipsisOutlined
+      css={css`
+        transform: rotate(90deg);
+        &:hover {
+          cursor: pointer;
+        }
+      `}
+      iconSize="xl"
+      iconColor={theme.colorTextLabel}
+    />
+  );
+};
+
+const WhatIfHeaderMenu = ({
+  dashboardId,
+  selectedSimulation,
+  onSelectSimulation,
+  onSaveClick,
+  onSaveAsNewClick,
+  hasModifications,
+  refreshTrigger,
+  addDangerToast,
+}: WhatIfHeaderMenuProps) => {
+  const theme = useTheme();
+  const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+  const [simulations, setSimulations] = useState<WhatIfSimulation[]>([]);
+  const [loading, setLoading] = useState(false);
+
+  const loadSimulations = useCallback(async () => {
+    setLoading(true);
+    try {
+      const result = await fetchSimulations(dashboardId);
+      setSimulations(result);
+    } catch (error) {
+      addDangerToast(t('Failed to load saved simulations'));
+    } finally {
+      setLoading(false);
+    }
+  }, [dashboardId, addDangerToast]);
+
+  useEffect(() => {
+    loadSimulations();
+  }, [loadSimulations, refreshTrigger]);
+
+  const handleMenuClick = useCallback(
+    ({ key }: { key: Key }) => {
+      const keyStr = String(key);
+
+      if (keyStr === MenuKeys.SaveSimulation) {
+        onSaveClick();
+        setIsDropdownVisible(false);
+      } else if (keyStr === MenuKeys.SaveAsNew) {
+        onSaveAsNewClick();
+        setIsDropdownVisible(false);
+      } else if (keyStr.startsWith('load-sim-')) {
+        const simId = parseInt(keyStr.replace('load-sim-', ''), 10);
+        const sim = simulations.find(s => s.id === simId);
+        if (sim) {
+          onSelectSimulation(sim);
+        }
+        setIsDropdownVisible(false);
+      } else if (keyStr === 'clear-simulation') {
+        onSelectSimulation(null);
+        setIsDropdownVisible(false);
+      }
+    },
+    [simulations, onSelectSimulation, onSaveClick, onSaveAsNewClick],
+  );
+
+  const simulationMenuItems =
+    simulations.length > 0
+      ? [
+          ...(selectedSimulation
+            ? [
+                {
+                  key: 'clear-simulation',
+                  label: t('Clear current simulation'),
+                  icon: <Icons.CloseOutlined />,
+                },
+                { type: 'divider' as const },
+              ]
+            : []),
+          ...simulations.map(sim => ({
+            key: `load-sim-${sim.id}`,
+            label: (
+              <div
+                css={css`
+                  display: flex;
+                  justify-content: space-between;
+                  align-items: center;
+                  gap: ${theme.sizeUnit * 2}px;
+                `}
+              >
+                <span
+                  css={css`
+                    flex: 1;
+                    overflow: hidden;
+                    text-overflow: ellipsis;
+                    white-space: nowrap;
+                  `}
+                >
+                  {sim.name}
+                  {selectedSimulation?.id === sim.id && (
+                    <Icons.CheckOutlined
+                      css={css`
+                        margin-left: ${theme.sizeUnit}px;
+                        color: ${theme.colorSuccess};
+                      `}
+                    />
+                  )}
+                </span>
+                {sim.description && (
+                  <Tooltip title={sim.description}>
+                    <Icons.InfoCircleOutlined
+                      onClick={e => e.stopPropagation()}
+                      css={css`
+                        color: ${theme.colorTextSecondary};
+                        font-size: ${theme.fontSizeSM}px;
+                      `}
+                    />
+                  </Tooltip>
+                )}
+              </div>
+            ),
+          })),
+        ]
+      : [
+          {
+            key: 'no-simulations',
+            label: t('No saved simulations'),
+            disabled: true,
+          },
+        ];
+
+  const menuItems = [
+    {
+      type: 'submenu' as const,
+      key: MenuKeys.LoadSimulation,
+      label: loading ? t('Loading...') : t('Load simulation'),
+      icon: <Icons.FolderOpenOutlined />,
+      children: simulationMenuItems,
+    },
+    {
+      key: MenuKeys.SaveSimulation,
+      label: selectedSimulation ? t('Update simulation') : t('Save 
simulation'),
+      icon: <Icons.SaveOutlined />,
+      disabled: !hasModifications,
+    },
+    ...(selectedSimulation
+      ? [
+          {
+            key: MenuKeys.SaveAsNew,
+            label: t('Save as new'),
+            icon: <Icons.PlusOutlined />,
+            disabled: !hasModifications,
+          },
+        ]
+      : []),
+    { type: 'divider' as const },
+    {
+      key: MenuKeys.ManageSimulations,
+      label: <Link to="/whatif/simulations/">{t('Manage simulations')}</Link>,
+      icon: <Icons.SettingOutlined />,
+    },
+  ];
+
+  return (
+    <NoAnimationDropdown
+      popupRender={() => (
+        <Menu
+          onClick={handleMenuClick}
+          data-test="what-if-header-menu"
+          selectable={false}
+          items={menuItems}
+        />
+      )}
+      trigger={['click']}
+      placement="bottomRight"
+      open={isDropdownVisible}
+      onOpenChange={visible => setIsDropdownVisible(visible)}
+    >
+      <Button
+        buttonStyle="link"
+        aria-label={t('More Options')}
+        aria-haspopup="true"
+        css={css`
+          padding: ${theme.sizeUnit}px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+        `}
+      >
+        <VerticalDotsTrigger />
+      </Button>
+    </NoAnimationDropdown>
+  );
+};
+
+export default WhatIfHeaderMenu;
diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
index 5718de1fc2..ed3cbf9812 100644
--- a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
+++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx
@@ -30,14 +30,18 @@ import {
 } from '@superset-ui/core/components';
 import Slider from '@superset-ui/core/components/Slider';
 import { Icons } from '@superset-ui/core/components/Icons';
+import { useToasts } from 'src/components/MessageToasts/withToasts';
 import { useNumericColumns } from 'src/dashboard/util/useNumericColumns';
 import { RootState, Datasource } from 'src/dashboard/types';
 import WhatIfAIInsights from './WhatIfAIInsights';
 import HarryPotterWandLoader from './HarryPotterWandLoader';
 import FilterButton from './FilterButton';
 import ModificationsDisplay from './ModificationsDisplay';
+import WhatIfHeaderMenu from './WhatIfHeaderMenu';
+import SaveSimulationModal from './SaveSimulationModal';
 import { useWhatIfFilters } from './useWhatIfFilters';
 import { useWhatIfApply } from './useWhatIfApply';
+import { WhatIfSimulation } from './whatIfApi';
 import {
   SLIDER_MIN,
   SLIDER_MAX,
@@ -61,6 +65,7 @@ import {
   ColumnSelectWrapper,
   FiltersSection,
   FilterTagsContainer,
+  HeaderButtonsContainer,
 } from './styles';
 
 export { WHAT_IF_PANEL_WIDTH };
@@ -72,6 +77,10 @@ interface WhatIfPanelProps {
 
 const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => {
   const theme = useTheme();
+  const { addSuccessToast, addDangerToast } = useToasts();
+
+  // Get dashboard ID from Redux
+  const dashboardId = useSelector((state: RootState) => 
state.dashboardInfo.id);
 
   // Local state for column selection and slider
   const [selectedColumn, setSelectedColumn] = useState<string | undefined>(
@@ -80,6 +89,12 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
   const [sliderValue, setSliderValue] = useState<number>(SLIDER_DEFAULT);
   const [enableCascadingEffects, setEnableCascadingEffects] = useState(false);
 
+  // Simulation state
+  const [selectedSimulation, setSelectedSimulation] =
+    useState<WhatIfSimulation | null>(null);
+  const [saveModalVisible, setSaveModalVisible] = useState(false);
+  const [simulationRefreshTrigger, setSimulationRefreshTrigger] = useState(0);
+
   // Custom hook for filter management
   const {
     filters,
@@ -105,6 +120,9 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
     handleApply,
     handleDismissLoader,
     aiInsightsModifications,
+    loadModificationsDirectly,
+    clearModifications,
+    interpretAbortRef,
   } = useWhatIfApply({
     selectedColumn,
     sliderValue,
@@ -167,6 +185,65 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
     setEnableCascadingEffects(e.target.checked);
   }, []);
 
+  // Handle loading a saved simulation
+  const handleLoadSimulation = useCallback(
+    (simulation: WhatIfSimulation | null) => {
+      setSelectedSimulation(simulation);
+      if (simulation && simulation.modifications.length > 0) {
+        const firstMod = simulation.modifications[0];
+        setSelectedColumn(firstMod.column);
+        setSliderValue((firstMod.multiplier - 1) * 100);
+        setEnableCascadingEffects(simulation.cascadingEffectsEnabled);
+
+        // Convert to extended modifications with isAISuggested flag
+        // First modification is the user's, rest are AI-suggested
+        const extendedModifications = simulation.modifications.map(
+          (mod, index) => ({
+            column: mod.column,
+            multiplier: mod.multiplier,
+            filters: mod.filters,
+            isAISuggested: index > 0,
+          }),
+        );
+
+        // Load all modifications directly and trigger chart queries + 
/interpret
+        loadModificationsDirectly(extendedModifications);
+      } else if (!simulation) {
+        // Clear state when deselecting
+        setSelectedColumn(undefined);
+        setSliderValue(SLIDER_DEFAULT);
+        setEnableCascadingEffects(false);
+        clearFilters();
+        clearModifications();
+      }
+    },
+    [clearFilters, loadModificationsDirectly, clearModifications],
+  );
+
+  // Handle saving a simulation
+  const handleSaveSimulation = useCallback((simulation: WhatIfSimulation) => {
+    setSelectedSimulation(simulation);
+    setSimulationRefreshTrigger(prev => prev + 1);
+  }, []);
+
+  // Track if we're saving as new (vs updating existing)
+  const [isSavingAsNew, setIsSavingAsNew] = useState(false);
+
+  const handleOpenSaveModal = useCallback(() => {
+    setIsSavingAsNew(false);
+    setSaveModalVisible(true);
+  }, []);
+
+  const handleOpenSaveAsNewModal = useCallback(() => {
+    setIsSavingAsNew(true);
+    setSaveModalVisible(true);
+  }, []);
+
+  const handleCloseSaveModal = useCallback(() => {
+    setSaveModalVisible(false);
+    setIsSavingAsNew(false);
+  }, []);
+
   const isApplyDisabled =
     !selectedColumn || sliderValue === SLIDER_DEFAULT || isLoadingSuggestions;
 
@@ -182,9 +259,21 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
           />
           {t('What-if playground')}
         </PanelTitle>
-        <CloseButton onClick={onClose} aria-label={t('Close')}>
-          <Icons.CloseOutlined iconSize="m" />
-        </CloseButton>
+        <HeaderButtonsContainer>
+          <WhatIfHeaderMenu
+            dashboardId={dashboardId}
+            selectedSimulation={selectedSimulation}
+            onSelectSimulation={handleLoadSimulation}
+            onSaveClick={handleOpenSaveModal}
+            onSaveAsNewClick={handleOpenSaveAsNewModal}
+            hasModifications={appliedModifications.length > 0}
+            refreshTrigger={simulationRefreshTrigger}
+            addDangerToast={addDangerToast}
+          />
+          <CloseButton onClick={onClose} aria-label={t('Close')}>
+            <Icons.CloseOutlined iconSize="m" />
+          </CloseButton>
+        </HeaderButtonsContainer>
       </PanelHeader>
 
       <PanelContent>
@@ -313,6 +402,7 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
             key={applyCounter}
             affectedChartIds={affectedChartIds}
             modifications={aiInsightsModifications}
+            abortRef={interpretAbortRef}
           />
         )}
       </PanelContent>
@@ -320,6 +410,18 @@ const WhatIfPanel = ({ onClose, topOffset }: 
WhatIfPanelProps) => {
       {isLoadingSuggestions && (
         <HarryPotterWandLoader onDismiss={handleDismissLoader} />
       )}
+
+      <SaveSimulationModal
+        show={saveModalVisible}
+        onHide={handleCloseSaveModal}
+        onSaved={handleSaveSimulation}
+        dashboardId={dashboardId}
+        modifications={appliedModifications}
+        cascadingEffectsEnabled={enableCascadingEffects}
+        existingSimulation={isSavingAsNew ? null : selectedSimulation}
+        addSuccessToast={addSuccessToast}
+        addDangerToast={addDangerToast}
+      />
     </PanelContainer>
   );
 };
diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts
index 07b19317f0..4f43c41e30 100644
--- a/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts
+++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/styles.ts
@@ -205,3 +205,9 @@ export const FilterTagsContainer = styled.div`
   flex-wrap: wrap;
   gap: ${({ theme }) => theme.sizeUnit}px;
 `;
+
+export const HeaderButtonsContainer = styled.div`
+  display: flex;
+  align-items: center;
+  gap: ${({ theme }) => theme.sizeUnit}px;
+`;
diff --git 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfApply.ts 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfApply.ts
index 7587b6dc14..b595f21e88 100644
--- a/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfApply.ts
+++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/useWhatIfApply.ts
@@ -45,6 +45,12 @@ export interface UseWhatIfApplyReturn {
   handleApply: () => Promise<void>;
   handleDismissLoader: () => void;
   aiInsightsModifications: WhatIfModification[];
+  loadModificationsDirectly: (
+    modifications: ExtendedWhatIfModification[],
+  ) => void;
+  clearModifications: () => void;
+  /** Ref to register an abort function from WhatIfAIInsights */
+  interpretAbortRef: React.MutableRefObject<(() => void) | null>;
 }
 
 /**
@@ -74,6 +80,9 @@ export function useWhatIfApply({
   // AbortController for cancelling in-flight /suggest_related requests
   const suggestionsAbortControllerRef = useRef<AbortController | null>(null);
 
+  // Ref to hold the abort function from WhatIfAIInsights for /interpret 
requests
+  const interpretAbortRef = useRef<(() => void) | null>(null);
+
   const { numericColumns, columnToChartIds } = useNumericColumns();
   const dashboardInfo = useSelector((state: RootState) => state.dashboardInfo);
 
@@ -88,8 +97,9 @@ export function useWhatIfApply({
   const handleApply = useCallback(async () => {
     if (!selectedColumn) return;
 
-    // Cancel any in-flight suggestions request
+    // Cancel any in-flight requests
     suggestionsAbortControllerRef.current?.abort();
+    interpretAbortRef.current?.();
 
     // Immediately clear previous results and increment counter to reset AI 
insights component
     setAppliedModifications([]);
@@ -208,6 +218,71 @@ export function useWhatIfApply({
     setIsLoadingSuggestions(false);
   }, []);
 
+  /**
+   * Load modifications directly without fetching AI suggestions.
+   * Used when loading a saved simulation.
+   */
+  const loadModificationsDirectly = useCallback(
+    (modifications: ExtendedWhatIfModification[]) => {
+      // Cancel any in-flight requests
+      suggestionsAbortControllerRef.current?.abort();
+      interpretAbortRef.current?.();
+      setIsLoadingSuggestions(false);
+
+      // Increment counter to reset AI insights component
+      setApplyCounter(c => c + 1);
+
+      setAppliedModifications(modifications);
+
+      // Collect all affected chart IDs from all modifications
+      const allAffectedChartIds = new Set<number>();
+      modifications.forEach(mod => {
+        const chartIds = columnToChartIds.get(mod.column) || [];
+        chartIds.forEach(id => allAffectedChartIds.add(id));
+      });
+      const chartIdsArray = Array.from(allAffectedChartIds);
+
+      // Save original chart data before applying what-if modifications
+      chartIdsArray.forEach(chartId => {
+        dispatch(saveOriginalChartData(chartId));
+      });
+
+      // Set the what-if modifications in Redux state
+      dispatch(
+        setWhatIfModifications(
+          modifications.map(mod => ({
+            column: mod.column,
+            multiplier: mod.multiplier,
+            filters: mod.filters,
+          })),
+        ),
+      );
+
+      // Trigger queries for all affected charts
+      chartIdsArray.forEach(chartId => {
+        dispatch(triggerQuery(true, chartId));
+      });
+
+      // Set affected chart IDs to enable AI insights
+      setAffectedChartIds(chartIdsArray);
+    },
+    [dispatch, columnToChartIds],
+  );
+
+  /**
+   * Clear all modifications and reset state.
+   */
+  const clearModifications = useCallback(() => {
+    // Cancel any in-flight requests
+    suggestionsAbortControllerRef.current?.abort();
+    interpretAbortRef.current?.();
+    setIsLoadingSuggestions(false);
+
+    setAppliedModifications([]);
+    setAffectedChartIds([]);
+    dispatch(setWhatIfModifications([]));
+  }, [dispatch]);
+
   // Memoize modifications array for WhatIfAIInsights to prevent unnecessary 
re-renders
   const aiInsightsModifications = useMemo(
     () =>
@@ -227,6 +302,9 @@ export function useWhatIfApply({
     handleApply,
     handleDismissLoader,
     aiInsightsModifications,
+    loadModificationsDirectly,
+    clearModifications,
+    interpretAbortRef,
   };
 }
 
diff --git 
a/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts 
b/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts
index cc1b247d93..40dcf724ab 100644
--- a/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts
+++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/whatIfApi.ts
@@ -23,11 +23,43 @@ import {
   WhatIfInterpretResponse,
   ChartComparison,
   WhatIfFilter,
+  WhatIfModification,
   WhatIfSuggestRelatedRequest,
   WhatIfSuggestRelatedResponse,
   SuggestedModification,
 } from './types';
 
+// 
=============================================================================
+// Simulation CRUD Types
+// 
=============================================================================
+
+export interface WhatIfSimulation {
+  id: number;
+  uuid: string;
+  name: string;
+  description?: string | null;
+  dashboardId: number;
+  modifications: WhatIfModification[];
+  cascadingEffectsEnabled: boolean;
+  createdOn?: string | null;
+  changedOn?: string | null;
+}
+
+export interface CreateSimulationRequest {
+  name: string;
+  description?: string;
+  dashboardId: number;
+  modifications: WhatIfModification[];
+  cascadingEffectsEnabled: boolean;
+}
+
+export interface UpdateSimulationRequest {
+  name?: string;
+  description?: string;
+  modifications?: WhatIfModification[];
+  cascadingEffectsEnabled?: boolean;
+}
+
 interface ApiResponse {
   result: {
     summary: string;
@@ -137,3 +169,160 @@ export async function fetchRelatedColumnSuggestions(
     explanation: result.explanation,
   };
 }
+
+// 
=============================================================================
+// Simulation CRUD API Functions
+// 
=============================================================================
+
+interface SimulationListResponse {
+  result: Array<{
+    id: number;
+    uuid: string;
+    name: string;
+    description?: string | null;
+    dashboard_id?: number;
+    modifications: Array<{
+      column: string;
+      multiplier: number;
+      filters?: Array<{
+        col: string;
+        op: string;
+        val: string | number | boolean | Array<string | number>;
+      }>;
+    }>;
+    cascading_effects_enabled: boolean;
+    created_on?: string | null;
+    changed_on?: string | null;
+  }>;
+}
+
+interface SimulationCreateResponse {
+  id: number;
+  uuid: string;
+}
+
+export async function fetchAllSimulations(): Promise<WhatIfSimulation[]> {
+  const response = await SupersetClient.get({
+    endpoint: '/api/v1/what_if/simulations',
+  });
+
+  const data = response.json as SimulationListResponse;
+  return data.result.map(sim => ({
+    id: sim.id,
+    uuid: sim.uuid,
+    name: sim.name,
+    description: sim.description,
+    dashboardId: sim.dashboard_id ?? 0,
+    modifications: sim.modifications.map(mod => ({
+      column: mod.column,
+      multiplier: mod.multiplier,
+      filters: mod.filters?.map(f => ({
+        col: f.col,
+        op: f.op as WhatIfFilter['op'],
+        val: f.val,
+      })),
+    })),
+    cascadingEffectsEnabled: sim.cascading_effects_enabled,
+    createdOn: sim.created_on,
+    changedOn: sim.changed_on,
+  }));
+}
+
+export async function fetchSimulations(
+  dashboardId: number,
+): Promise<WhatIfSimulation[]> {
+  const response = await SupersetClient.get({
+    endpoint: `/api/v1/what_if/simulations/dashboard/${dashboardId}`,
+  });
+
+  const data = response.json as SimulationListResponse;
+  return data.result.map(sim => ({
+    id: sim.id,
+    uuid: sim.uuid,
+    name: sim.name,
+    description: sim.description,
+    dashboardId,
+    modifications: sim.modifications.map(mod => ({
+      column: mod.column,
+      multiplier: mod.multiplier,
+      filters: mod.filters?.map(f => ({
+        col: f.col,
+        op: f.op as WhatIfFilter['op'],
+        val: f.val,
+      })),
+    })),
+    cascadingEffectsEnabled: sim.cascading_effects_enabled,
+    createdOn: sim.created_on,
+    changedOn: sim.changed_on,
+  }));
+}
+
+export async function createSimulation(
+  request: CreateSimulationRequest,
+): Promise<WhatIfSimulation> {
+  const response = await SupersetClient.post({
+    endpoint: '/api/v1/what_if/simulations',
+    jsonPayload: {
+      name: request.name,
+      description: request.description,
+      dashboard_id: request.dashboardId,
+      modifications: request.modifications.map(mod => ({
+        column: mod.column,
+        multiplier: mod.multiplier,
+        filters: mod.filters?.map(f => ({
+          col: f.col,
+          op: f.op,
+          val: f.val,
+        })),
+      })),
+      cascading_effects_enabled: request.cascadingEffectsEnabled,
+    },
+  });
+
+  const data = response.json as SimulationCreateResponse;
+  return {
+    id: data.id,
+    uuid: data.uuid,
+    name: request.name,
+    description: request.description,
+    dashboardId: request.dashboardId,
+    modifications: request.modifications,
+    cascadingEffectsEnabled: request.cascadingEffectsEnabled,
+  };
+}
+
+export async function updateSimulation(
+  simulationId: number,
+  request: UpdateSimulationRequest,
+): Promise<void> {
+  const payload: Record<string, unknown> = {};
+
+  if (request.name !== undefined) payload.name = request.name;
+  if (request.description !== undefined)
+    payload.description = request.description;
+  if (request.cascadingEffectsEnabled !== undefined) {
+    payload.cascading_effects_enabled = request.cascadingEffectsEnabled;
+  }
+  if (request.modifications !== undefined) {
+    payload.modifications = request.modifications.map(mod => ({
+      column: mod.column,
+      multiplier: mod.multiplier,
+      filters: mod.filters?.map(f => ({
+        col: f.col,
+        op: f.op,
+        val: f.val,
+      })),
+    }));
+  }
+
+  await SupersetClient.put({
+    endpoint: `/api/v1/what_if/simulations/${simulationId}`,
+    jsonPayload: payload,
+  });
+}
+
+export async function deleteSimulation(simulationId: number): Promise<void> {
+  await SupersetClient.delete({
+    endpoint: `/api/v1/what_if/simulations/${simulationId}`,
+  });
+}
diff --git a/superset-frontend/src/pages/WhatIfSimulationList/index.tsx 
b/superset-frontend/src/pages/WhatIfSimulationList/index.tsx
new file mode 100644
index 0000000000..102f92bbd2
--- /dev/null
+++ b/superset-frontend/src/pages/WhatIfSimulationList/index.tsx
@@ -0,0 +1,385 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useMemo, useState, useEffect, useCallback } from 'react';
+import { Link } from 'react-router-dom';
+import { t, SupersetClient } from '@superset-ui/core';
+import { css, styled } from '@apache-superset/core/ui';
+import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
+
+import {
+  ConfirmStatusChange,
+  DeleteModal,
+  Empty,
+  Skeleton,
+} from '@superset-ui/core/components';
+import {
+  ListView,
+  ListViewActionsBar,
+  type ListViewProps,
+  type ListViewActionProps,
+} from 'src/components';
+import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
+import withToasts from 'src/components/MessageToasts/withToasts';
+
+import {
+  fetchAllSimulations,
+  deleteSimulation,
+  WhatIfSimulation,
+} from 'src/dashboard/components/WhatIfDrawer/whatIfApi';
+
+const PAGE_SIZE = 25;
+
+interface WhatIfSimulationListProps {
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+}
+
+interface DashboardInfo {
+  id: number;
+  dashboard_title: string;
+  slug: string | null;
+}
+
+const PageContainer = styled.div`
+  padding: ${({ theme }) => theme.gridUnit * 4}px;
+`;
+
+const EmptyContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: ${({ theme }) => theme.gridUnit * 16}px;
+`;
+
+function WhatIfSimulationList({
+  addDangerToast,
+  addSuccessToast,
+}: WhatIfSimulationListProps) {
+  const [simulations, setSimulations] = useState<WhatIfSimulation[]>([]);
+  const [dashboards, setDashboards] = useState<Record<number, DashboardInfo>>(
+    {},
+  );
+  const [loading, setLoading] = useState(true);
+  const [simulationCurrentlyDeleting, setSimulationCurrentlyDeleting] =
+    useState<WhatIfSimulation | null>(null);
+
+  const loadSimulations = useCallback(async () => {
+    setLoading(true);
+    try {
+      const result = await fetchAllSimulations();
+      setSimulations(result);
+
+      // Fetch dashboard info for all unique dashboard IDs
+      const dashboardIds = [...new Set(result.map(sim => sim.dashboardId))];
+      if (dashboardIds.length > 0) {
+        const dashboardInfos: Record<number, DashboardInfo> = {};
+        await Promise.all(
+          dashboardIds.map(async id => {
+            try {
+              const response = await SupersetClient.get({
+                endpoint: `/api/v1/dashboard/${id}`,
+              });
+              dashboardInfos[id] = {
+                id,
+                dashboard_title: response.json.result.dashboard_title,
+                slug: response.json.result.slug,
+              };
+            } catch {
+              dashboardInfos[id] = {
+                id,
+                dashboard_title: `Dashboard ${id}`,
+                slug: null,
+              };
+            }
+          }),
+        );
+        setDashboards(dashboardInfos);
+      }
+    } catch (error) {
+      addDangerToast(t('Failed to load simulations'));
+    } finally {
+      setLoading(false);
+    }
+  }, [addDangerToast]);
+
+  useEffect(() => {
+    loadSimulations();
+  }, [loadSimulations]);
+
+  const handleDelete = useCallback(
+    async (simulation: WhatIfSimulation) => {
+      try {
+        await deleteSimulation(simulation.id);
+        setSimulationCurrentlyDeleting(null);
+        addSuccessToast(t('Deleted: %s', simulation.name));
+        loadSimulations();
+      } catch (error) {
+        addDangerToast(t('Failed to delete simulation'));
+      }
+    },
+    [addSuccessToast, addDangerToast, loadSimulations],
+  );
+
+  const handleBulkDelete = useCallback(
+    async (simulationsToDelete: WhatIfSimulation[]) => {
+      try {
+        await Promise.all(
+          simulationsToDelete.map(sim => deleteSimulation(sim.id)),
+        );
+        addSuccessToast(
+          t('Deleted %s simulation(s)', simulationsToDelete.length),
+        );
+        loadSimulations();
+      } catch (error) {
+        addDangerToast(t('Failed to delete simulations'));
+      }
+    },
+    [addSuccessToast, addDangerToast, loadSimulations],
+  );
+
+  const menuData: SubMenuProps = {
+    name: t('What-If Simulations'),
+  };
+
+  const initialSort = [{ id: 'changedOn', desc: true }];
+
+  const columns = useMemo(
+    () => [
+      {
+        accessor: 'name',
+        Header: t('Name'),
+        size: 'xxl',
+        id: 'name',
+      },
+      {
+        accessor: 'description',
+        Header: t('Description'),
+        size: 'xl',
+        id: 'description',
+        Cell: ({
+          row: {
+            original: { description },
+          },
+        }: {
+          row: { original: WhatIfSimulation };
+        }) => description || '-',
+      },
+      {
+        accessor: 'dashboardId',
+        Header: t('Dashboard'),
+        size: 'lg',
+        id: 'dashboardId',
+        Cell: ({
+          row: {
+            original: { dashboardId },
+          },
+        }: {
+          row: { original: WhatIfSimulation };
+        }) => {
+          const dashboard = dashboards[dashboardId];
+          if (!dashboard) return `Dashboard ${dashboardId}`;
+          const url = dashboard.slug
+            ? `/superset/dashboard/${dashboard.slug}/`
+            : `/superset/dashboard/${dashboardId}/`;
+          return <Link to={url}>{dashboard.dashboard_title}</Link>;
+        },
+      },
+      {
+        accessor: 'modifications',
+        Header: t('Modifications'),
+        size: 'md',
+        id: 'modifications',
+        disableSortBy: true,
+        Cell: ({
+          row: {
+            original: { modifications },
+          },
+        }: {
+          row: { original: WhatIfSimulation };
+        }) => modifications.length,
+      },
+      {
+        accessor: 'changedOn',
+        Header: t('Last modified'),
+        size: 'lg',
+        id: 'changedOn',
+        Cell: ({
+          row: {
+            original: { changedOn },
+          },
+        }: {
+          row: { original: WhatIfSimulation };
+        }) => (changedOn ? dayjs(changedOn).format('ll') : '-'),
+      },
+      {
+        Cell: ({
+          row: { original },
+        }: {
+          row: { original: WhatIfSimulation };
+        }) => {
+          const dashboard = dashboards[original.dashboardId];
+          const dashboardUrl = dashboard?.slug
+            ? `/superset/dashboard/${dashboard.slug}/`
+            : `/superset/dashboard/${original.dashboardId}/`;
+
+          const handleOpen = () => {
+            window.location.href = dashboardUrl;
+          };
+          const handleDelete = () => setSimulationCurrentlyDeleting(original);
+
+          const actions = [
+            {
+              label: 'open-action',
+              tooltip: t('Open in Dashboard'),
+              placement: 'bottom',
+              icon: 'ExportOutlined',
+              onClick: handleOpen,
+            },
+            {
+              label: 'delete-action',
+              tooltip: t('Delete simulation'),
+              placement: 'bottom',
+              icon: 'DeleteOutlined',
+              onClick: handleDelete,
+            },
+          ];
+
+          return (
+            <ListViewActionsBar actions={actions as ListViewActionProps[]} />
+          );
+        },
+        Header: t('Actions'),
+        id: 'actions',
+        disableSortBy: true,
+      },
+    ],
+    [dashboards],
+  );
+
+  const emptyState = {
+    title: t('No simulations yet'),
+    image: 'filter-results.svg',
+    description: t(
+      'Create your first What-If simulation from the What-If panel in a 
dashboard.',
+    ),
+  };
+
+  if (loading) {
+    return (
+      <>
+        <SubMenu {...menuData} />
+        <PageContainer>
+          <Skeleton active />
+        </PageContainer>
+      </>
+    );
+  }
+
+  if (simulations.length === 0) {
+    return (
+      <>
+        <SubMenu {...menuData} />
+        <EmptyContainer>
+          <Empty
+            image="simple"
+            description={
+              <>
+                <div
+                  css={css`
+                    font-weight: 600;
+                    margin-bottom: 8px;
+                  `}
+                >
+                  {t('No simulations yet')}
+                </div>
+                <div>
+                  {t(
+                    'Create your first What-If simulation from the What-If 
panel in a dashboard.',
+                  )}
+                </div>
+              </>
+            }
+          />
+        </EmptyContainer>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <SubMenu {...menuData} />
+      {simulationCurrentlyDeleting && (
+        <DeleteModal
+          description={t(
+            'Are you sure you want to delete %s?',
+            simulationCurrentlyDeleting.name,
+          )}
+          onConfirm={() => {
+            if (simulationCurrentlyDeleting) {
+              handleDelete(simulationCurrentlyDeleting);
+            }
+          }}
+          onHide={() => setSimulationCurrentlyDeleting(null)}
+          open
+          title={t('Delete Simulation?')}
+        />
+      )}
+      <ConfirmStatusChange
+        title={t('Please confirm')}
+        description={t(
+          'Are you sure you want to delete the selected simulations?',
+        )}
+        onConfirm={handleBulkDelete}
+      >
+        {confirmDelete => {
+          const bulkActions: ListViewProps['bulkActions'] = [
+            {
+              key: 'delete',
+              name: t('Delete'),
+              onSelect: confirmDelete,
+              type: 'danger',
+            },
+          ];
+
+          return (
+            <ListView<WhatIfSimulation>
+              bulkActions={bulkActions}
+              bulkSelectEnabled={false}
+              columns={columns}
+              count={simulations.length}
+              data={simulations}
+              emptyState={emptyState}
+              fetchData={() => {}}
+              addDangerToast={addDangerToast}
+              addSuccessToast={addSuccessToast}
+              refreshData={loadSimulations}
+              initialSort={initialSort}
+              loading={loading}
+              pageSize={PAGE_SIZE}
+            />
+          );
+        }}
+      </ConfirmStatusChange>
+    </>
+  );
+}
+
+export default withToasts(WhatIfSimulationList);
diff --git a/superset-frontend/src/views/routes.tsx 
b/superset-frontend/src/views/routes.tsx
index db4fff7214..e88014a1c5 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -138,6 +138,13 @@ const RowLevelSecurityList = lazy(
     ),
 );
 
+const WhatIfSimulationList = lazy(
+  () =>
+    import(
+      /* webpackChunkName: "WhatIfSimulationList" */ 
'src/pages/WhatIfSimulationList'
+    ),
+);
+
 const RolesList = lazy(
   () => import(/* webpackChunkName: "RolesList" */ 'src/pages/RolesList'),
 );
@@ -289,6 +296,10 @@ export const routes: Routes = [
     path: '/rowlevelsecurity/list',
     Component: RowLevelSecurityList,
   },
+  {
+    path: '/whatif/simulations/',
+    Component: WhatIfSimulationList,
+  },
   {
     path: '/sqllab/',
     Component: SqlLab,
diff --git a/superset/daos/what_if_simulation.py 
b/superset/daos/what_if_simulation.py
new file mode 100644
index 0000000000..cd478d9fe9
--- /dev/null
+++ b/superset/daos/what_if_simulation.py
@@ -0,0 +1,106 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""DAO for What-If Simulation persistence."""
+
+from __future__ import annotations
+
+import logging
+from typing import Optional
+
+from superset.daos.base import BaseDAO
+from superset.extensions import db
+from superset.utils.core import get_user_id
+from superset.what_if.models import WhatIfSimulation
+
+logger = logging.getLogger(__name__)
+
+
+class WhatIfSimulationDAO(BaseDAO[WhatIfSimulation]):
+    """Data access object for What-If Simulations."""
+
+    @classmethod
+    def find_by_dashboard_and_user(
+        cls,
+        dashboard_id: int,
+        user_id: Optional[int] = None,
+    ) -> list[WhatIfSimulation]:
+        """
+        Find all simulations for a dashboard owned by a specific user.
+
+        :param dashboard_id: The dashboard ID
+        :param user_id: The user ID (defaults to current user)
+        :returns: List of simulations
+        """
+        if user_id is None:
+            user_id = get_user_id()
+
+        return (
+            db.session.query(WhatIfSimulation)
+            .filter(
+                WhatIfSimulation.dashboard_id == dashboard_id,
+                WhatIfSimulation.user_id == user_id,
+            )
+            .order_by(WhatIfSimulation.changed_on.desc())
+            .all()
+        )
+
+    @classmethod
+    def find_all_for_user(
+        cls,
+        user_id: Optional[int] = None,
+    ) -> list[WhatIfSimulation]:
+        """
+        Find all simulations owned by a user across all dashboards.
+
+        :param user_id: The user ID (defaults to current user)
+        :returns: List of simulations
+        """
+        if user_id is None:
+            user_id = get_user_id()
+
+        return (
+            db.session.query(WhatIfSimulation)
+            .filter(WhatIfSimulation.user_id == user_id)
+            .order_by(WhatIfSimulation.changed_on.desc())
+            .all()
+        )
+
+    @classmethod
+    def validate_name_uniqueness(
+        cls,
+        name: str,
+        dashboard_id: int,
+        user_id: int,
+        simulation_id: Optional[int] = None,
+    ) -> bool:
+        """
+        Validate if simulation name is unique for this dashboard/user combo.
+
+        :param name: The simulation name
+        :param dashboard_id: The dashboard ID
+        :param user_id: The user ID
+        :param simulation_id: Optional simulation ID (for updates)
+        :returns: True if unique, False otherwise
+        """
+        query = db.session.query(WhatIfSimulation).filter(
+            WhatIfSimulation.name == name,
+            WhatIfSimulation.dashboard_id == dashboard_id,
+            WhatIfSimulation.user_id == user_id,
+        )
+        if simulation_id:
+            query = query.filter(WhatIfSimulation.id != simulation_id)
+        return not db.session.query(query.exists()).scalar()
diff --git 
a/superset/migrations/versions/2025-12-19_10-00_b8f3a2c9d1e5_add_what_if_simulations.py
 
b/superset/migrations/versions/2025-12-19_10-00_b8f3a2c9d1e5_add_what_if_simulations.py
new file mode 100644
index 0000000000..7c361ceea7
--- /dev/null
+++ 
b/superset/migrations/versions/2025-12-19_10-00_b8f3a2c9d1e5_add_what_if_simulations.py
@@ -0,0 +1,116 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""add_what_if_simulations
+
+Revision ID: b8f3a2c9d1e5
+Revises: a9c01ec10479
+Create Date: 2025-12-19 10:00:00.000000
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import mysql
+from sqlalchemy_utils import UUIDType
+
+from superset.migrations.shared.utils import (
+    create_fks_for_table,
+    create_table,
+    drop_table,
+)
+
+# revision identifiers, used by Alembic.
+revision = "b8f3a2c9d1e5"
+down_revision = "a9c01ec10479"
+
+
+def upgrade():
+    create_table(
+        "what_if_simulations",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("uuid", UUIDType(binary=True), nullable=False),
+        sa.Column("name", sa.String(length=256), nullable=False),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("dashboard_id", sa.Integer(), nullable=False),
+        sa.Column("user_id", sa.Integer(), nullable=False),
+        sa.Column(
+            "modifications_json",
+            sa.Text().with_variant(mysql.MEDIUMTEXT(), "mysql"),
+            nullable=False,
+        ),
+        sa.Column(
+            "cascading_effects_enabled",
+            sa.Boolean(),
+            server_default=sa.false(),
+            nullable=False,
+        ),
+        sa.Column("created_on", sa.DateTime(), nullable=True),
+        sa.Column("changed_on", sa.DateTime(), nullable=True),
+        sa.Column("created_by_fk", sa.Integer(), nullable=True),
+        sa.Column("changed_by_fk", sa.Integer(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("uuid"),
+    )
+
+    # Create index for fast lookup by dashboard_id + user_id
+    op.create_index(
+        "ix_what_if_simulations_dashboard_user",
+        "what_if_simulations",
+        ["dashboard_id", "user_id"],
+    )
+
+    # Create foreign key constraints
+    create_fks_for_table(
+        "fk_what_if_simulations_dashboard_id_dashboards",
+        "what_if_simulations",
+        "dashboards",
+        ["dashboard_id"],
+        ["id"],
+        ondelete="CASCADE",
+    )
+
+    create_fks_for_table(
+        "fk_what_if_simulations_user_id_ab_user",
+        "what_if_simulations",
+        "ab_user",
+        ["user_id"],
+        ["id"],
+    )
+
+    create_fks_for_table(
+        "fk_what_if_simulations_created_by_fk_ab_user",
+        "what_if_simulations",
+        "ab_user",
+        ["created_by_fk"],
+        ["id"],
+    )
+
+    create_fks_for_table(
+        "fk_what_if_simulations_changed_by_fk_ab_user",
+        "what_if_simulations",
+        "ab_user",
+        ["changed_by_fk"],
+        ["id"],
+    )
+
+
+def downgrade():
+    op.drop_index(
+        "ix_what_if_simulations_dashboard_user",
+        table_name="what_if_simulations",
+    )
+    drop_table("what_if_simulations")
diff --git a/superset/what_if/api.py b/superset/what_if/api.py
index 5ed560b0cf..6c67086b74 100644
--- a/superset/what_if/api.py
+++ b/superset/what_if/api.py
@@ -25,14 +25,30 @@ from flask_appbuilder.api import expose, protect, safe
 from marshmallow import ValidationError
 
 from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
+from superset.daos.what_if_simulation import WhatIfSimulationDAO
 from superset.extensions import event_logger
+from superset.utils import json
 from superset.views.base_api import BaseSupersetApi, statsd_metrics
 from superset.what_if.commands.interpret import WhatIfInterpretCommand
+from superset.what_if.commands.simulation_create import 
CreateWhatIfSimulationCommand
+from superset.what_if.commands.simulation_delete import 
DeleteWhatIfSimulationCommand
+from superset.what_if.commands.simulation_update import 
UpdateWhatIfSimulationCommand
 from superset.what_if.commands.suggest_related import 
WhatIfSuggestRelatedCommand
-from superset.what_if.exceptions import OpenRouterAPIError, 
OpenRouterConfigError
+from superset.what_if.exceptions import (
+    OpenRouterAPIError,
+    OpenRouterConfigError,
+    WhatIfSimulationCreateFailedError,
+    WhatIfSimulationDeleteFailedError,
+    WhatIfSimulationForbiddenError,
+    WhatIfSimulationInvalidError,
+    WhatIfSimulationNotFoundError,
+    WhatIfSimulationUpdateFailedError,
+)
 from superset.what_if.schemas import (
     WhatIfInterpretRequestSchema,
     WhatIfInterpretResponseSchema,
+    WhatIfSimulationPostSchema,
+    WhatIfSimulationPutSchema,
     WhatIfSuggestRelatedRequestSchema,
     WhatIfSuggestRelatedResponseSchema,
 )
@@ -52,9 +68,9 @@ class WhatIfRestApi(BaseSupersetApi):
     method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
 
     @expose("/interpret", methods=("POST",))
-    @event_logger.log_this
     @protect()
     @safe
+    @event_logger.log_this
     @statsd_metrics
     def interpret(self) -> Response:
         """Generate AI interpretation of what-if analysis results.
@@ -121,9 +137,9 @@ class WhatIfRestApi(BaseSupersetApi):
             return self.response_400(message=str(ex))
 
     @expose("/suggest_related", methods=("POST",))
-    @event_logger.log_this
     @protect()
     @safe
+    @event_logger.log_this
     @statsd_metrics
     def suggest_related(self) -> Response:
         """Get AI suggestions for related column modifications.
@@ -189,3 +205,269 @@ class WhatIfRestApi(BaseSupersetApi):
         except ValueError as ex:
             logger.warning("Invalid request: %s", ex)
             return self.response_400(message=str(ex))
+
+    # =========================================================================
+    # Simulation CRUD Endpoints
+    # =========================================================================
+
+    @expose("/simulations", methods=("GET",))
+    @protect()
+    @safe
+    @event_logger.log_this
+    @statsd_metrics
+    def list_all_simulations(self) -> Response:
+        """List all saved simulations for the current user across all 
dashboards.
+        ---
+        get:
+          summary: List all What-If simulations for current user
+          responses:
+            200:
+              description: List of simulations
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        type: array
+            401:
+              $ref: '#/components/responses/401'
+          security:
+            - jwt: []
+        """
+        simulations = WhatIfSimulationDAO.find_all_for_user()
+        result = [
+            {
+                "id": sim.id,
+                "uuid": str(sim.uuid),
+                "name": sim.name,
+                "description": sim.description,
+                "dashboard_id": sim.dashboard_id,
+                "modifications": json.loads(sim.modifications_json),
+                "cascading_effects_enabled": sim.cascading_effects_enabled,
+                "created_on": sim.created_on.isoformat() if sim.created_on 
else None,
+                "changed_on": sim.changed_on.isoformat() if sim.changed_on 
else None,
+            }
+            for sim in simulations
+        ]
+        return self.response(200, result=result)
+
+    @expose("/simulations/dashboard/<int:dashboard_id>", methods=("GET",))
+    @protect()
+    @safe
+    @event_logger.log_this
+    @statsd_metrics
+    def list_simulations(self, dashboard_id: int) -> Response:
+        """List all saved simulations for a dashboard (current user only).
+        ---
+        get:
+          summary: List What-If simulations for a dashboard
+          parameters:
+          - in: path
+            name: dashboard_id
+            schema:
+              type: integer
+            required: true
+          responses:
+            200:
+              description: List of simulations
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        type: array
+            401:
+              $ref: '#/components/responses/401'
+          security:
+            - jwt: []
+        """
+        simulations = 
WhatIfSimulationDAO.find_by_dashboard_and_user(dashboard_id)
+        result = [
+            {
+                "id": sim.id,
+                "uuid": str(sim.uuid),
+                "name": sim.name,
+                "description": sim.description,
+                "modifications": json.loads(sim.modifications_json),
+                "cascading_effects_enabled": sim.cascading_effects_enabled,
+                "created_on": sim.created_on.isoformat() if sim.created_on 
else None,
+                "changed_on": sim.changed_on.isoformat() if sim.changed_on 
else None,
+            }
+            for sim in simulations
+        ]
+        return self.response(200, result=result)
+
+    @expose("/simulations", methods=("POST",))
+    @protect()
+    @safe
+    @event_logger.log_this
+    @statsd_metrics
+    def create_simulation(self) -> Response:
+        """Create a new What-If simulation.
+        ---
+        post:
+          summary: Save a new What-If simulation
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/WhatIfSimulationPostSchema'
+          responses:
+            201:
+              description: Simulation created
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      id:
+                        type: integer
+                      uuid:
+                        type: string
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            422:
+              $ref: '#/components/responses/422'
+          security:
+            - jwt: []
+        """
+        try:
+            data = WhatIfSimulationPostSchema().load(request.json)
+        except ValidationError as ex:
+            logger.warning("Invalid request data: %s", ex.messages)
+            return self.response_400(message=str(ex.messages))
+
+        # Serialize modifications to JSON
+        data["modifications_json"] = json.dumps(data.pop("modifications"))
+
+        try:
+            simulation = CreateWhatIfSimulationCommand(data).run()
+            return self.response(
+                201,
+                id=simulation.id,
+                uuid=str(simulation.uuid),
+            )
+        except WhatIfSimulationInvalidError as ex:
+            return self.response_422(message=ex.normalized_messages())
+        except WhatIfSimulationCreateFailedError as ex:
+            logger.error("Error creating simulation: %s", ex)
+            return self.response_422(message=str(ex))
+
+    @expose("/simulations/<int:pk>", methods=("PUT",))
+    @protect()
+    @safe
+    @event_logger.log_this
+    @statsd_metrics
+    def update_simulation(self, pk: int) -> Response:
+        """Update a What-If simulation.
+        ---
+        put:
+          summary: Update a What-If simulation
+          parameters:
+          - in: path
+            name: pk
+            schema:
+              type: integer
+            required: true
+          requestBody:
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/WhatIfSimulationPutSchema'
+          responses:
+            200:
+              description: Simulation updated
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      id:
+                        type: integer
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            403:
+              $ref: '#/components/responses/403'
+            404:
+              $ref: '#/components/responses/404'
+            422:
+              $ref: '#/components/responses/422'
+          security:
+            - jwt: []
+        """
+        try:
+            data = WhatIfSimulationPutSchema().load(request.json)
+        except ValidationError as ex:
+            logger.warning("Invalid request data: %s", ex.messages)
+            return self.response_400(message=str(ex.messages))
+
+        # Serialize modifications to JSON if present
+        if "modifications" in data:
+            data["modifications_json"] = json.dumps(data.pop("modifications"))
+
+        try:
+            simulation = UpdateWhatIfSimulationCommand(pk, data).run()
+            return self.response(200, id=simulation.id)
+        except WhatIfSimulationNotFoundError:
+            return self.response_404()
+        except WhatIfSimulationForbiddenError:
+            return self.response_403()
+        except WhatIfSimulationInvalidError as ex:
+            return self.response_422(message=ex.normalized_messages())
+        except WhatIfSimulationUpdateFailedError as ex:
+            logger.error("Error updating simulation: %s", ex)
+            return self.response_422(message=str(ex))
+
+    @expose("/simulations/<int:pk>", methods=("DELETE",))
+    @protect()
+    @safe
+    @event_logger.log_this
+    @statsd_metrics
+    def delete_simulation(self, pk: int) -> Response:
+        """Delete a What-If simulation.
+        ---
+        delete:
+          summary: Delete a What-If simulation
+          parameters:
+          - in: path
+            name: pk
+            schema:
+              type: integer
+            required: true
+          responses:
+            200:
+              description: Simulation deleted
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+            401:
+              $ref: '#/components/responses/401'
+            403:
+              $ref: '#/components/responses/403'
+            404:
+              $ref: '#/components/responses/404'
+          security:
+            - jwt: []
+        """
+        try:
+            DeleteWhatIfSimulationCommand([pk]).run()
+            return self.response(200, message="OK")
+        except WhatIfSimulationNotFoundError:
+            return self.response_404()
+        except WhatIfSimulationForbiddenError:
+            return self.response_403()
+        except WhatIfSimulationDeleteFailedError as ex:
+            logger.error("Error deleting simulation: %s", ex)
+            return self.response_422(message=str(ex))
diff --git a/superset/what_if/commands/interpret.py 
b/superset/what_if/commands/interpret.py
index 1611c83d28..fd0fdc6730 100644
--- a/superset/what_if/commands/interpret.py
+++ b/superset/what_if/commands/interpret.py
@@ -18,7 +18,6 @@
 
 from __future__ import annotations
 
-import json
 import logging
 from typing import Any
 
@@ -26,6 +25,7 @@ import httpx
 from flask import current_app
 
 from superset.commands.base import BaseCommand
+from superset.utils import json
 from superset.what_if.exceptions import OpenRouterAPIError, 
OpenRouterConfigError
 
 logger = logging.getLogger(__name__)
diff --git a/superset/what_if/commands/simulation_create.py 
b/superset/what_if/commands/simulation_create.py
new file mode 100644
index 0000000000..e84606bb38
--- /dev/null
+++ b/superset/what_if/commands/simulation_create.py
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Create What-If Simulation command."""
+
+from __future__ import annotations
+
+import logging
+from functools import partial
+from typing import Any
+
+from flask_appbuilder.models.sqla import Model
+from marshmallow import ValidationError
+
+from superset.commands.base import BaseCommand
+from superset.daos.what_if_simulation import WhatIfSimulationDAO
+from superset.utils.core import get_user_id
+from superset.utils.decorators import on_error, transaction
+from superset.what_if.exceptions import (
+    WhatIfSimulationCreateFailedError,
+    WhatIfSimulationInvalidError,
+    WhatIfSimulationNameUniquenessError,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class CreateWhatIfSimulationCommand(BaseCommand):
+    """Command to create a new What-If simulation."""
+
+    def __init__(self, data: dict[str, Any]):
+        self._properties = data.copy()
+
+    @transaction(on_error=partial(on_error, 
reraise=WhatIfSimulationCreateFailedError))
+    def run(self) -> Model:
+        self.validate()
+        user_id = get_user_id()
+        self._properties["user_id"] = user_id
+        return WhatIfSimulationDAO.create(attributes=self._properties)
+
+    def validate(self) -> None:
+        exceptions: list[ValidationError] = []
+
+        name = self._properties.get("name", "")
+        dashboard_id = self._properties.get("dashboard_id")
+        user_id = get_user_id()
+
+        # Validate name uniqueness for this dashboard/user
+        if not WhatIfSimulationDAO.validate_name_uniqueness(
+            name, dashboard_id, user_id
+        ):
+            exceptions.append(WhatIfSimulationNameUniquenessError())
+
+        if exceptions:
+            raise WhatIfSimulationInvalidError(exceptions=exceptions)
diff --git a/superset/what_if/commands/simulation_delete.py 
b/superset/what_if/commands/simulation_delete.py
new file mode 100644
index 0000000000..c26ac43bd0
--- /dev/null
+++ b/superset/what_if/commands/simulation_delete.py
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Delete What-If Simulation command."""
+
+from __future__ import annotations
+
+import logging
+from functools import partial
+
+from superset.commands.base import BaseCommand
+from superset.daos.what_if_simulation import WhatIfSimulationDAO
+from superset.utils.core import get_user_id
+from superset.utils.decorators import on_error, transaction
+from superset.what_if.exceptions import (
+    WhatIfSimulationDeleteFailedError,
+    WhatIfSimulationForbiddenError,
+    WhatIfSimulationNotFoundError,
+)
+from superset.what_if.models import WhatIfSimulation
+
+logger = logging.getLogger(__name__)
+
+
+class DeleteWhatIfSimulationCommand(BaseCommand):
+    """Command to delete What-If simulation(s)."""
+
+    def __init__(self, simulation_ids: list[int]):
+        self._simulation_ids = simulation_ids
+        self._models: list[WhatIfSimulation] = []
+
+    @transaction(on_error=partial(on_error, 
reraise=WhatIfSimulationDeleteFailedError))
+    def run(self) -> None:
+        self.validate()
+        WhatIfSimulationDAO.delete(self._models)
+
+    def validate(self) -> None:
+        user_id = get_user_id()
+        self._models = WhatIfSimulationDAO.find_by_ids(self._simulation_ids)
+
+        if len(self._models) != len(self._simulation_ids):
+            raise WhatIfSimulationNotFoundError()
+
+        # Check ownership of all simulations
+        for model in self._models:
+            if model.user_id != user_id:
+                raise WhatIfSimulationForbiddenError()
diff --git a/superset/what_if/commands/simulation_update.py 
b/superset/what_if/commands/simulation_update.py
new file mode 100644
index 0000000000..49c846dac0
--- /dev/null
+++ b/superset/what_if/commands/simulation_update.py
@@ -0,0 +1,82 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Update What-If Simulation command."""
+
+from __future__ import annotations
+
+import logging
+from functools import partial
+from typing import Any, Optional
+
+from flask_appbuilder.models.sqla import Model
+from marshmallow import ValidationError
+
+from superset.commands.base import BaseCommand
+from superset.daos.what_if_simulation import WhatIfSimulationDAO
+from superset.utils.core import get_user_id
+from superset.utils.decorators import on_error, transaction
+from superset.what_if.exceptions import (
+    WhatIfSimulationForbiddenError,
+    WhatIfSimulationInvalidError,
+    WhatIfSimulationNameUniquenessError,
+    WhatIfSimulationNotFoundError,
+    WhatIfSimulationUpdateFailedError,
+)
+from superset.what_if.models import WhatIfSimulation
+
+logger = logging.getLogger(__name__)
+
+
+class UpdateWhatIfSimulationCommand(BaseCommand):
+    """Command to update a What-If simulation."""
+
+    def __init__(self, simulation_id: int, data: dict[str, Any]):
+        self._simulation_id = simulation_id
+        self._properties = data.copy()
+        self._model: Optional[WhatIfSimulation] = None
+
+    @transaction(on_error=partial(on_error, 
reraise=WhatIfSimulationUpdateFailedError))
+    def run(self) -> Model:
+        self.validate()
+        return WhatIfSimulationDAO.update(self._model, 
attributes=self._properties)
+
+    def validate(self) -> None:
+        exceptions: list[ValidationError] = []
+
+        # Fetch model
+        self._model = WhatIfSimulationDAO.find_by_id(self._simulation_id)
+        if not self._model:
+            raise WhatIfSimulationNotFoundError()
+
+        # Check ownership
+        user_id = get_user_id()
+        if self._model.user_id != user_id:
+            raise WhatIfSimulationForbiddenError()
+
+        # Validate name uniqueness if name is being updated
+        name = self._properties.get("name")
+        if name and name != self._model.name:
+            if not WhatIfSimulationDAO.validate_name_uniqueness(
+                name,
+                self._model.dashboard_id,
+                user_id,
+                self._simulation_id,
+            ):
+                exceptions.append(WhatIfSimulationNameUniquenessError())
+
+        if exceptions:
+            raise WhatIfSimulationInvalidError(exceptions=exceptions)
diff --git a/superset/what_if/commands/suggest_related.py 
b/superset/what_if/commands/suggest_related.py
index 7cd5334a70..e0341f6e3c 100644
--- a/superset/what_if/commands/suggest_related.py
+++ b/superset/what_if/commands/suggest_related.py
@@ -18,7 +18,6 @@
 
 from __future__ import annotations
 
-import json
 import logging
 from typing import Any
 
@@ -26,6 +25,7 @@ import httpx
 from flask import current_app
 
 from superset.commands.base import BaseCommand
+from superset.utils import json
 from superset.what_if.exceptions import OpenRouterAPIError, 
OpenRouterConfigError
 
 logger = logging.getLogger(__name__)
diff --git a/superset/what_if/exceptions.py b/superset/what_if/exceptions.py
index 666423f29c..621b7c93b6 100644
--- a/superset/what_if/exceptions.py
+++ b/superset/what_if/exceptions.py
@@ -16,6 +16,16 @@
 # under the License.
 """What-If Analysis exceptions."""
 
+from flask_babel import lazy_gettext as _
+from marshmallow import ValidationError
+
+from superset.commands.exceptions import (
+    CommandException,
+    CommandInvalidError,
+    CreateFailedError,
+    DeleteFailedError,
+    ForbiddenError,
+)
 from superset.exceptions import SupersetException
 
 
@@ -35,3 +45,55 @@ class OpenRouterAPIError(WhatIfException):
 
     status = 502
     message = "Error communicating with OpenRouter API"
+
+
+# =============================================================================
+# Simulation persistence exceptions
+# =============================================================================
+
+
+class WhatIfSimulationNameUniquenessError(ValidationError):
+    """Validation error for simulation name already exists."""
+
+    def __init__(self) -> None:
+        super().__init__(
+            [_("Name must be unique for this dashboard")],
+            field_name="name",
+        )
+
+
+class WhatIfSimulationNotFoundError(CommandException):
+    """Raised when a simulation is not found."""
+
+    status = 404
+    message = _("What-If simulation not found.")
+
+
+class WhatIfSimulationInvalidError(CommandInvalidError):
+    """Raised when simulation parameters are invalid."""
+
+    message = _("What-If simulation parameters are invalid.")
+
+
+class WhatIfSimulationCreateFailedError(CreateFailedError):
+    """Raised when simulation creation fails."""
+
+    message = _("What-If simulation could not be created.")
+
+
+class WhatIfSimulationUpdateFailedError(CreateFailedError):
+    """Raised when simulation update fails."""
+
+    message = _("What-If simulation could not be updated.")
+
+
+class WhatIfSimulationDeleteFailedError(DeleteFailedError):
+    """Raised when simulation deletion fails."""
+
+    message = _("What-If simulation could not be deleted.")
+
+
+class WhatIfSimulationForbiddenError(ForbiddenError):
+    """Raised when user doesn't have permission to access a simulation."""
+
+    message = _("You do not have permission to access this simulation.")
diff --git a/superset/what_if/models.py b/superset/what_if/models.py
new file mode 100644
index 0000000000..5ae8224058
--- /dev/null
+++ b/superset/what_if/models.py
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""What-If Simulation persistence models."""
+
+from __future__ import annotations
+
+import json
+import uuid
+from typing import Any
+
+from flask_appbuilder import Model
+from sqlalchemy import Boolean, Column, ForeignKey, Index, Integer, String, 
Text
+from sqlalchemy.orm import relationship
+from sqlalchemy_utils import UUIDType
+
+from superset import security_manager
+from superset.models.helpers import AuditMixinNullable
+
+
+class WhatIfSimulation(Model, AuditMixinNullable):
+    """Saved What-If simulation configuration."""
+
+    __tablename__ = "what_if_simulations"
+
+    id = Column(Integer, primary_key=True)
+    uuid = Column(
+        UUIDType(binary=True), default=uuid.uuid4, unique=True, nullable=False
+    )
+    name = Column(String(256), nullable=False)
+    description = Column(Text, nullable=True)
+    dashboard_id = Column(
+        Integer, ForeignKey("dashboards.id", ondelete="CASCADE"), 
nullable=False
+    )
+    user_id = Column(Integer, ForeignKey("ab_user.id"), nullable=False)
+
+    # JSON column storing modifications array
+    # Structure: [{"column": "...", "multiplier": 1.1, "filters": [...]}]
+    modifications_json = Column(Text, nullable=False)
+
+    # Whether cascading effects were enabled when saved
+    cascading_effects_enabled = Column(Boolean, default=False, nullable=False)
+
+    # Relationships
+    dashboard = relationship(
+        "Dashboard",
+        foreign_keys=[dashboard_id],
+        backref="what_if_simulations",
+    )
+    user = relationship(
+        security_manager.user_model,
+        foreign_keys=[user_id],
+    )
+
+    __table_args__ = (
+        Index("ix_what_if_simulations_dashboard_user", dashboard_id, user_id),
+    )
+
+    @property
+    def modifications(self) -> list[dict[str, Any]]:
+        """Parse and return modifications from JSON."""
+        if self.modifications_json:
+            return json.loads(self.modifications_json)
+        return []
+
+    @modifications.setter
+    def modifications(self, value: list[dict[str, Any]]) -> None:
+        """Serialize modifications to JSON."""
+        self.modifications_json = json.dumps(value)
+
+    def __repr__(self) -> str:
+        return f"WhatIfSimulation<{self.name}>"
diff --git a/superset/what_if/schemas.py b/superset/what_if/schemas.py
index 87212daede..7bd8ebba5a 100644
--- a/superset/what_if/schemas.py
+++ b/superset/what_if/schemas.py
@@ -248,3 +248,82 @@ class WhatIfSuggestRelatedResponseSchema(Schema):
         load_default=None,
         metadata={"description": "Overall explanation of the relationship 
analysis"},
     )
+
+
+# =============================================================================
+# Simulation CRUD Schemas
+# =============================================================================
+
+
+class WhatIfSimulationPostSchema(Schema):
+    """Schema for creating a What-If simulation."""
+
+    name = fields.String(
+        required=True,
+        metadata={"description": "Name of the saved simulation"},
+    )
+    description = fields.String(
+        required=False,
+        allow_none=True,
+        load_default=None,
+        metadata={"description": "Optional description"},
+    )
+    dashboard_id = fields.Integer(
+        required=True,
+        metadata={"description": "ID of the dashboard this simulation belongs 
to"},
+    )
+    modifications = fields.List(
+        fields.Nested(ModificationSchema),
+        required=True,
+        metadata={"description": "List of column modifications"},
+    )
+    cascading_effects_enabled = fields.Boolean(
+        required=False,
+        load_default=False,
+        metadata={"description": "Whether AI cascading effects were enabled"},
+    )
+
+
+class WhatIfSimulationPutSchema(Schema):
+    """Schema for updating a What-If simulation."""
+
+    name = fields.String(
+        required=False,
+        metadata={"description": "Name of the saved simulation"},
+    )
+    description = fields.String(
+        required=False,
+        allow_none=True,
+        metadata={"description": "Optional description"},
+    )
+    modifications = fields.List(
+        fields.Nested(ModificationSchema),
+        required=False,
+        metadata={"description": "List of column modifications"},
+    )
+    cascading_effects_enabled = fields.Boolean(
+        required=False,
+        metadata={"description": "Whether AI cascading effects were enabled"},
+    )
+
+
+class WhatIfSimulationResponseSchema(Schema):
+    """Schema for simulation response."""
+
+    id = fields.Integer(metadata={"description": "Simulation ID"})
+    uuid = fields.String(metadata={"description": "Simulation UUID"})
+    name = fields.String(metadata={"description": "Simulation name"})
+    description = fields.String(
+        allow_none=True,
+        metadata={"description": "Simulation description"},
+    )
+    dashboard_id = fields.Integer(metadata={"description": "Dashboard ID"})
+    modifications = fields.List(
+        fields.Nested(ModificationSchema),
+        metadata={"description": "Saved modifications"},
+    )
+    cascading_effects_enabled = fields.Boolean(
+        metadata={"description": "Whether cascading effects were enabled"},
+    )
+    created_on = fields.DateTime(metadata={"description": "Creation 
timestamp"})
+    changed_on = fields.DateTime(metadata={"description": "Last modified 
timestamp"})

Reply via email to