diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx
index 4d42814e53dd5dd07206d076f144883706a475c2..e7ebb2094700f80cd5ab9b8ab5c587a2f0f65233 100644
--- a/packages/desktop-client/src/components/modals/EditRule.jsx
+++ b/packages/desktop-client/src/components/modals/EditRule.jsx
@@ -1,6 +1,8 @@
 import React, { useState, useEffect, useRef, useCallback } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
 
+import { v4 as uuid } from 'uuid';
+
 import {
   initiallyLoadPayees,
   setUndoEnabled,
@@ -26,10 +28,11 @@ import {
   amountToInteger,
 } from 'loot-core/src/shared/util';
 
+import { useFeatureFlag } from '../../hooks/useFeatureFlag';
 import { useSelected, SelectedProvider } from '../../hooks/useSelected';
-import { SvgAdd, SvgSubtract } from '../../icons/v0';
+import { SvgDelete, SvgAdd, SvgSubtract } from '../../icons/v0';
 import { SvgInformationOutline } from '../../icons/v1';
-import { theme } from '../../style';
+import { styles, theme } from '../../style';
 import { Button } from '../common/Button';
 import { Modal } from '../common/Modal';
 import { Select } from '../common/Select';
@@ -122,6 +125,19 @@ export function OpSelect({
   );
 }
 
+function SplitAmountMethodSelect({ options, style, value, onChange }) {
+  return (
+    <View style={{ color: theme.pageTextPositive, ...style }}>
+      <Select
+        bare
+        options={options}
+        value={value}
+        onChange={value => onChange('method', value)}
+      />
+    </View>
+  );
+}
+
 function EditorButtons({ onAdd, onDelete }) {
   return (
     <>
@@ -310,8 +326,25 @@ const actionFields = [
   'date',
   'amount',
 ].map(field => [field, mapField(field)]);
+const parentOnlyFields = ['amount', 'cleared', 'account', 'date'];
+const splitActionFields = actionFields.filter(
+  ([field]) => !parentOnlyFields.includes(field),
+);
+const splitAmountTypes = [
+  ['fixed-amount', 'a fixed amount'],
+  ['fixed-percent', 'a fixed percentage'],
+  ['remainder', 'an equal portion of the remainder'],
+];
 function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
-  const { field, op, value, type, error, inputKey = 'initial' } = action;
+  const {
+    field,
+    op,
+    value,
+    type,
+    error,
+    inputKey = 'initial',
+    options,
+  } = action;
 
   return (
     <Editor style={editorStyle} error={error}>
@@ -324,7 +357,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
           </View>
 
           <FieldSelect
-            fields={actionFields}
+            fields={options?.splitIndex ? splitActionFields : actionFields}
             value={field}
             onChange={onChange}
           />
@@ -340,6 +373,30 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
             />
           </View>
         </>
+      ) : op === 'set-split-amount' ? (
+        <>
+          <View style={{ padding: '5px 10px', lineHeight: '1em' }}>
+            allocate
+          </View>
+
+          <SplitAmountMethodSelect
+            options={splitAmountTypes}
+            value={options.method}
+            onChange={onChange}
+          />
+
+          <View style={{ flex: 1 }}>
+            {options.method !== 'remainder' && (
+              <GenericInput
+                key={inputKey}
+                field={field}
+                type="number"
+                value={value}
+                onChange={v => onChange('value', v)}
+              />
+            )}
+          </View>
+        </>
       ) : op === 'link-schedule' ? (
         <>
           <View
@@ -355,10 +412,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
       ) : null}
 
       <Stack direction="row">
-        <EditorButtons
-          onAdd={onAdd}
-          onDelete={op !== 'link-schedule' && onDelete}
-        />
+        <EditorButtons onAdd={onAdd} onDelete={op === 'set' && onDelete} />
       </Stack>
     </Editor>
   );
@@ -587,6 +641,9 @@ function ConditionsList({
   );
 }
 
+const getActions = splits => splits.flatMap(s => s.actions);
+const getUnparsedActions = splits => getActions(splits).map(unparse);
+
 // TODO:
 // * Dont touch child transactions?
 
@@ -606,17 +663,32 @@ const conditionFields = [
   ]);
 
 export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
+  const splitsEnabled = useFeatureFlag('splitsInRules');
   const [conditions, setConditions] = useState(
     defaultRule.conditions.map(parse),
   );
-  const [actions, setActions] = useState(defaultRule.actions.map(parse));
+  const [actionSplits, setActionSplits] = useState(() => {
+    const parsedActions = defaultRule.actions.map(parse);
+    return parsedActions.reduce(
+      (acc, action) => {
+        const splitIndex = action.options?.splitIndex ?? 0;
+        acc[splitIndex] = acc[splitIndex] ?? { id: uuid(), actions: [] };
+        acc[splitIndex].actions.push(action);
+        return acc;
+      },
+      // The pre-split group is always there
+      [{ id: uuid(), actions: [] }],
+    );
+  });
   const [stage, setStage] = useState(defaultRule.stage);
   const [conditionsOp, setConditionsOp] = useState(defaultRule.conditionsOp);
   const [transactions, setTransactions] = useState([]);
   const dispatch = useDispatch();
   const scrollableEl = useRef();
 
-  const isSchedule = actions.some(action => action.op === 'link-schedule');
+  const isSchedule = getActions(actionSplits).some(
+    action => action.op === 'link-schedule',
+  );
 
   useEffect(() => {
     dispatch(initiallyLoadPayees());
@@ -643,9 +715,11 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
 
       if (filters.length > 0) {
         const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
+        const parentOnlyCondition =
+          actionSplits.length > 1 ? { is_child: false } : {};
         const { data: transactions } = await runQuery(
           q('transactions')
-            .filter({ [conditionsOpKey]: filters })
+            .filter({ [conditionsOpKey]: filters, ...parentOnlyCondition })
             .select('*'),
         );
         setTransactions(transactions);
@@ -654,49 +728,71 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
       }
     }
     run();
-  }, [actions, conditions, conditionsOp]);
+  }, [actionSplits, conditions, conditionsOp]);
 
   const selectedInst = useSelected('transactions', transactions, []);
 
   function addInitialAction() {
-    addAction(-1);
+    addActionToSplitAfterIndex(0, -1);
   }
 
-  function addAction(index) {
-    let fields = actionFields.map(f => f[0]);
-    for (const action of actions) {
-      fields = fields.filter(f => f !== action.field);
+  function addActionToSplitAfterIndex(splitIndex, actionIndex) {
+    let newAction;
+    if (splitIndex && !actionSplits[splitIndex]?.actions?.length) {
+      actionSplits[splitIndex] = { id: uuid(), actions: [] };
+      newAction = {
+        op: 'set-split-amount',
+        options: { method: 'remainder', splitIndex },
+        value: null,
+      };
+    } else {
+      const fieldsArray = splitIndex === 0 ? actionFields : splitActionFields;
+      let fields = fieldsArray.map(f => f[0]);
+      for (const action of actionSplits[splitIndex].actions) {
+        fields = fields.filter(f => f !== action.field);
+      }
+      const field = fields[0] || 'category';
+      newAction = {
+        type: FIELD_TYPES.get(field),
+        field,
+        op: 'set',
+        value: null,
+        options: { splitIndex },
+      };
     }
-    const field = fields[0] || 'category';
 
-    const copy = [...actions];
-    copy.splice(index + 1, 0, {
-      type: FIELD_TYPES.get(field),
-      field,
-      op: 'set',
-      value: null,
-    });
-    setActions(copy);
+    const actionsCopy = [...actionSplits[splitIndex].actions];
+    actionsCopy.splice(actionIndex + 1, 0, newAction);
+    const copy = [...actionSplits];
+    copy[splitIndex] = { ...actionSplits[splitIndex], actions: actionsCopy };
+    setActionSplits(copy);
   }
 
   function onChangeAction(action, field, value) {
-    setActions(
-      updateValue(actions, action, () => {
-        const a = { ...action };
-        a[field] = value;
-
-        if (field === 'field') {
-          a.type = FIELD_TYPES.get(a.field);
-          a.value = null;
-          return newInput(a);
-        } else if (field === 'op') {
-          a.value = null;
-          a.inputKey = '' + Math.random();
-          return newInput(a);
-        }
+    setActionSplits(
+      actionSplits.map(({ id, actions }) => ({
+        id,
+        actions: updateValue(actions, action, () => {
+          const a = { ...action };
+          if (field === 'method') {
+            a.options = { ...a.options, method: value };
+          } else {
+            a[field] = value;
+
+            if (field === 'field') {
+              a.type = FIELD_TYPES.get(a.field);
+              a.value = null;
+              return newInput(a);
+            } else if (field === 'op') {
+              a.value = null;
+              a.inputKey = '' + Math.random();
+              return newInput(a);
+            }
+          }
 
-        return a;
-      }),
+          return a;
+        }),
+      })),
     );
   }
 
@@ -709,16 +805,51 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
   }
 
   function onRemoveAction(action) {
-    setActions(actions.filter(a => a !== action));
+    setActionSplits(splits =>
+      splits.map(({ id, actions }) => ({
+        id,
+        actions: actions.filter(a => a !== action),
+      })),
+    );
+  }
+
+  function onRemoveSplit(splitIndexToRemove) {
+    setActionSplits(splits => {
+      const copy = [];
+      splits.forEach(({ id }, index) => {
+        if (index === splitIndexToRemove) {
+          return;
+        }
+        copy.push({ id, actions: [] });
+      });
+      getActions(splits).forEach(action => {
+        const currentSplitIndex = action.options?.splitIndex ?? 0;
+        if (currentSplitIndex === splitIndexToRemove) {
+          return;
+        }
+        const newSplitIndex =
+          currentSplitIndex > splitIndexToRemove
+            ? currentSplitIndex - 1
+            : currentSplitIndex;
+        copy[newSplitIndex].actions.push({
+          ...action,
+          options: { ...action.options, splitIndex: newSplitIndex },
+        });
+      });
+      return copy;
+    });
   }
 
   function onApply() {
+    const selectedTransactions = transactions.filter(({ id }) =>
+      selectedInst.items.has(id),
+    );
     send('rule-apply-actions', {
-      transactionIds: [...selectedInst.items],
-      actions: actions.map(unparse),
+      transactions: selectedTransactions,
+      actions: getUnparsedActions(actionSplits),
     }).then(() => {
       // This makes it refetch the transactions
-      setActions([...actions]);
+      setActionSplits([...actionSplits]);
     });
   }
 
@@ -728,7 +859,7 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
       stage,
       conditionsOp,
       conditions: conditions.map(unparse),
-      actions: actions.map(unparse),
+      actions: getUnparsedActions(actionSplits),
     };
 
     const method = rule.id ? 'rule-update' : 'rule-add';
@@ -740,7 +871,7 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
       }
 
       if (error.actionErrors) {
-        setActions(applyErrors(actions, error.actionErrors));
+        setActionSplits(applyErrors(actionSplits, error.actionErrors));
       }
     } else {
       // If adding a rule, we got back an id
@@ -759,6 +890,11 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
     borderRadius: 4,
   };
 
+  // Enable editing existing split rules even if the feature has since been disabled.
+  const showSplitButton = splitsEnabled
+    ? actionSplits.length > 0
+    : actionSplits.length > 1;
+
   return (
     <Modal
       title="Rule"
@@ -852,30 +988,112 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
                 Then apply these actions:
               </Text>
               <View style={{ flex: 1 }}>
-                {actions.length === 0 ? (
+                {actionSplits.length === 0 && (
                   <Button
                     style={{ alignSelf: 'flex-start' }}
                     onClick={addInitialAction}
                   >
                     Add action
                   </Button>
-                ) : (
-                  <Stack spacing={2} data-testid="action-list">
-                    {actions.map((action, i) => (
-                      <View key={i}>
-                        <ActionEditor
-                          ops={['set', 'link-schedule']}
-                          action={action}
-                          editorStyle={editorStyle}
-                          onChange={(name, value) => {
-                            onChangeAction(action, name, value);
-                          }}
-                          onDelete={() => onRemoveAction(action)}
-                          onAdd={() => addAction(i)}
-                        />
-                      </View>
-                    ))}
-                  </Stack>
+                )}
+                <Stack spacing={2} data-testid="action-split-list">
+                  {actionSplits.map(({ id, actions }, splitIndex) => (
+                    <View
+                      key={id}
+                      nativeStyle={
+                        actionSplits.length > 1
+                          ? {
+                              borderColor: theme.tableBorder,
+                              borderWidth: '1px',
+                              borderRadius: '5px',
+                              padding: '5px',
+                            }
+                          : {}
+                      }
+                    >
+                      {actionSplits.length > 1 && (
+                        <Stack
+                          direction="row"
+                          justify="space-between"
+                          spacing={1}
+                        >
+                          <Text
+                            style={{
+                              ...styles.verySmallText,
+                              marginBottom: '10px',
+                            }}
+                          >
+                            {splitIndex === 0
+                              ? 'Before split'
+                              : `Split ${splitIndex}`}
+                          </Text>
+                          {splitIndex && (
+                            <Button
+                              type="bare"
+                              onClick={() => onRemoveSplit(splitIndex)}
+                              style={{
+                                width: 20,
+                                height: 20,
+                              }}
+                              aria-label="Delete split"
+                            >
+                              <SvgDelete
+                                style={{
+                                  width: 8,
+                                  height: 8,
+                                  color: 'inherit',
+                                }}
+                              />
+                            </Button>
+                          )}
+                        </Stack>
+                      )}
+                      <Stack spacing={2} data-testid="action-list">
+                        {actions.map((action, actionIndex) => (
+                          <View key={actionIndex}>
+                            <ActionEditor
+                              ops={['set', 'link-schedule']}
+                              action={action}
+                              editorStyle={editorStyle}
+                              onChange={(name, value) => {
+                                onChangeAction(action, name, value);
+                              }}
+                              onDelete={() => onRemoveAction(action)}
+                              onAdd={() =>
+                                addActionToSplitAfterIndex(
+                                  splitIndex,
+                                  actionIndex,
+                                )
+                              }
+                            />
+                          </View>
+                        ))}
+                      </Stack>
+
+                      {actions.length === 0 && (
+                        <Button
+                          style={{ alignSelf: 'flex-start', marginTop: 5 }}
+                          onClick={() =>
+                            addActionToSplitAfterIndex(splitIndex, -1)
+                          }
+                        >
+                          Add action
+                        </Button>
+                      )}
+                    </View>
+                  ))}
+                </Stack>
+                {showSplitButton && (
+                  <Button
+                    style={{ alignSelf: 'flex-start', marginTop: 15 }}
+                    onClick={() => {
+                      addActionToSplitAfterIndex(actionSplits.length, -1);
+                    }}
+                  >
+                    {actionSplits.length > 1
+                      ? 'Add another split'
+                      : 'Split into multiple transactions'}
+                  </Button>
                 )}
               </View>
             </View>
@@ -905,7 +1123,10 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
 
               <SimpleTransactionsTable
                 transactions={transactions}
-                fields={getTransactionFields(conditions, actions)}
+                fields={getTransactionFields(
+                  conditions,
+                  getActions(actionSplits),
+                )}
                 style={{
                   border: '1px solid ' + theme.tableBorder,
                   borderRadius: '6px 6px 0 0',
diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx
index d58b9b623065e991d8d1fe2efc926b156027f5ed..094af86241b656e077e638aa874638785bbf1b9c 100644
--- a/packages/desktop-client/src/components/settings/Experimental.tsx
+++ b/packages/desktop-client/src/components/settings/Experimental.tsx
@@ -99,6 +99,7 @@ export function ExperimentalFeatures() {
               Goal templates
             </FeatureToggle>
             <FeatureToggle flag="simpleFinSync">SimpleFIN sync</FeatureToggle>
+            <FeatureToggle flag="splitsInRules">Splits in rules</FeatureToggle>
           </View>
         ) : (
           <LinkButton
diff --git a/packages/desktop-client/src/components/spreadsheet/CellValue.tsx b/packages/desktop-client/src/components/spreadsheet/CellValue.tsx
index 0c453a9744a7f37de00e466a825de043a59541a2..7a298c3ffdf8b349312a89f787e837b24ee3da47 100644
--- a/packages/desktop-client/src/components/spreadsheet/CellValue.tsx
+++ b/packages/desktop-client/src/components/spreadsheet/CellValue.tsx
@@ -5,7 +5,7 @@ import { type CSSProperties, styles } from '../../style';
 import { Text } from '../common/Text';
 import { ConditionalPrivacyFilter } from '../PrivacyFilter';
 
-import { useFormat } from './useFormat';
+import { type FormatType, useFormat } from './useFormat';
 import { useSheetName } from './useSheetName';
 import { useSheetValue } from './useSheetValue';
 
@@ -13,7 +13,7 @@ import { type Binding } from '.';
 
 type CellValueProps = {
   binding: string | Binding;
-  type?: string;
+  type?: FormatType;
   formatter?: (value) => ReactNode;
   style?: CSSProperties;
   getStyle?: (value) => CSSProperties;
diff --git a/packages/desktop-client/src/components/spreadsheet/useFormat.ts b/packages/desktop-client/src/components/spreadsheet/useFormat.ts
index 28d0c1f0bd17503b056d4664ba7f7b8e86d865f6..d1599dd6a710d73e5f57207d812632f429e66923 100644
--- a/packages/desktop-client/src/components/spreadsheet/useFormat.ts
+++ b/packages/desktop-client/src/components/spreadsheet/useFormat.ts
@@ -4,9 +4,15 @@ import { useSelector } from 'react-redux';
 import { selectNumberFormat } from 'loot-core/src/client/selectors';
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
+export type FormatType =
+  | 'string'
+  | 'number'
+  | 'financial'
+  | 'financial-with-sign';
+
 function format(
   value: unknown,
-  type = 'string',
+  type: FormatType = 'string',
   formatter?: Intl.NumberFormat,
 ): string {
   switch (type) {
@@ -49,7 +55,7 @@ export function useFormat() {
   const numberFormat = useSelector(selectNumberFormat);
 
   return useCallback(
-    (value: unknown, type = 'string') =>
+    (value: unknown, type: FormatType = 'string') =>
       format(value, type, numberFormat.formatter),
     [numberFormat],
   );
diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx
index d1c98ec48f26b82ad7322b97837e0c6ad8a26161..5a51dd0f9f2ff3d4c2a679f78b9525e6edb3a5c0 100644
--- a/packages/desktop-client/src/components/table.tsx
+++ b/packages/desktop-client/src/components/table.tsx
@@ -40,7 +40,7 @@ import {
   mergeConditionalPrivacyFilterProps,
 } from './PrivacyFilter';
 import { type Binding } from './spreadsheet';
-import { useFormat } from './spreadsheet/useFormat';
+import { type FormatType, useFormat } from './spreadsheet/useFormat';
 import { useSheetValue } from './spreadsheet/useSheetValue';
 import { Tooltip, IntersectionBoundary } from './tooltips';
 
@@ -660,7 +660,7 @@ export function SelectCell({
 
 type SheetCellValueProps = {
   binding: Binding;
-  type: string;
+  type: FormatType;
   getValueStyle?: (value: string | number) => CSSProperties;
   formatExpr?: (value) => string;
   unformatExpr?: (value: string) => unknown;
diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
index aa939b53990435c6c0634be9f06d443a1c79b9bc..bda688058a8c25341f8b8ced7f5ae0513d5f8623 100644
--- a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
+++ b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx
@@ -992,19 +992,23 @@ function TransactionEditUnconnected(props) {
     return null;
   }
 
-  const onEdit = async transaction => {
-    let newTransaction = transaction;
+  const onEdit = async serializedTransaction => {
+    const transaction = deserializeTransaction(
+      serializedTransaction,
+      null,
+      dateFormat,
+    );
+
     // Run the rules to auto-fill in any data. Right now we only do
     // this on new transactions because that's how desktop works.
     if (isTemporary(transaction)) {
       const afterRules = await send('rules-run', { transaction });
       const diff = getChangedValues(transaction, afterRules);
 
-      newTransaction = { ...transaction };
       if (diff) {
         Object.keys(diff).forEach(field => {
-          if (newTransaction[field] == null) {
-            newTransaction[field] = diff[field];
+          if (transaction[field] == null) {
+            transaction[field] = diff[field];
           }
         });
       }
@@ -1012,7 +1016,7 @@ function TransactionEditUnconnected(props) {
 
     const { data: newTransactions } = updateTransaction(
       transactions,
-      deserializeTransaction(newTransaction, null, dateFormat),
+      transaction,
     );
     setTransactions(newTransactions);
   };
diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx
index 0472fa482479a16c93ebe5248daea3685c8d77f7..c99d1d75974e189b70cab3cb60d3e4f17918ae5e 100644
--- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx
+++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx
@@ -35,6 +35,8 @@ import {
   updateTransaction,
   deleteTransaction,
   addSplitTransaction,
+  groupTransaction,
+  ungroupTransactions,
 } from 'loot-core/src/shared/transactions';
 import {
   integerToCurrency,
@@ -705,6 +707,7 @@ function PayeeIcons({
 const Transaction = memo(function Transaction(props) {
   const {
     transaction: originalTransaction,
+    subtransactions,
     editing,
     showAccount,
     showBalance,
@@ -843,7 +846,8 @@ const Transaction = memo(function Transaction(props) {
       // Run the transaction through the formatting so that we know
       // it's always showing the formatted result
       setTransaction(serializeTransaction(deserialized, showZeroInDeposit));
-      onSave(deserialized);
+
+      onSave(deserialized, subtransactions);
     }
   }
 
@@ -1489,9 +1493,10 @@ function NewTransaction({
   const error = transactions[0].error;
   const isDeposit = transactions[0].amount > 0;
 
-  const emptyChildTransactions = transactions.filter(
-    t => t.parent_id === transactions[0].id && t.amount === 0,
+  const childTransactions = transactions.filter(
+    t => t.parent_id === transactions[0].id,
   );
+  const emptyChildTransactions = childTransactions.filter(t => t.amount === 0);
 
   return (
     <View
@@ -1513,6 +1518,7 @@ function NewTransaction({
           key={transaction.id}
           editing={editingTransaction === transaction.id}
           transaction={transaction}
+          subtransactions={transaction.is_parent ? childTransactions : null}
           showAccount={showAccount}
           showCategory={showCategory}
           showBalance={showBalance}
@@ -2064,18 +2070,28 @@ export const TransactionTable = forwardRef((props, ref) => {
   }, [props.onAdd, newNavigator.onEdit]);
 
   const onSave = useCallback(
-    async transaction => {
+    async (transaction, subtransactions = null) => {
       savePending.current = true;
 
+      let groupedTransaction = subtransactions
+        ? groupTransaction([transaction, ...subtransactions])
+        : transaction;
+
       if (isTemporaryId(transaction.id)) {
         if (props.onApplyRules) {
-          transaction = await props.onApplyRules(transaction);
+          groupedTransaction = await props.onApplyRules(groupedTransaction);
         }
 
         const newTrans = latestState.current.newTransactions;
-        setNewTransactions(updateTransaction(newTrans, transaction).data);
+        // Future refactor: we shouldn't need to iterate through the entire
+        // transaction list to ungroup, just the new transactions.
+        setNewTransactions(
+          ungroupTransactions(
+            updateTransaction(newTrans, groupedTransaction).data,
+          ),
+        );
       } else {
-        props.onSave(transaction);
+        props.onSave(groupedTransaction);
       }
     },
     [props.onSave],
diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts
index 4ba88ede7d7e86099c0a812d4a97fdc3248f957c..1395862d7896fb6953e7488bc47c24e58a76878e 100644
--- a/packages/desktop-client/src/hooks/useFeatureFlag.ts
+++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts
@@ -12,6 +12,7 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
   goalTemplatesEnabled: false,
   customReports: false,
   simpleFinSync: false,
+  splitsInRules: false,
 };
 
 export function useFeatureFlag(name: FeatureFlag): boolean {
diff --git a/packages/loot-core/src/server/accounts/rules.ts b/packages/loot-core/src/server/accounts/rules.ts
index 96b33b525126d1957bfa38fd3cc00e749b712690..68f0d67bffb609decc1bc4a856c53d63e7ff6a1a 100644
--- a/packages/loot-core/src/server/accounts/rules.ts
+++ b/packages/loot-core/src/server/accounts/rules.ts
@@ -12,9 +12,16 @@ import {
 } from '../../shared/months';
 import { sortNumbers, getApproxNumberThreshold } from '../../shared/rules';
 import { recurConfigToRSchedule } from '../../shared/schedules';
+import {
+  addSplitTransaction,
+  recalculateSplit,
+  splitTransaction,
+  ungroupTransaction,
+} from '../../shared/transactions';
 import { fastSetMerge } from '../../shared/util';
 import { RuleConditionEntity } from '../../types/models';
 import { RuleError } from '../errors';
+import * as prefs from '../prefs';
 import { Schedule as RSchedule } from '../util/rschedule';
 
 function assert(test, type, msg) {
@@ -418,9 +425,8 @@ export class Condition {
   }
 }
 
-type ActionOperator = 'set' | 'link-schedule';
-
-const ACTION_OPS: ActionOperator[] = ['set', 'link-schedule'];
+const ACTION_OPS = ['set', 'set-split-amount', 'link-schedule'] as const;
+type ActionOperator = (typeof ACTION_OPS)[number];
 
 export class Action {
   field;
@@ -442,6 +448,9 @@ export class Action {
       assert(typeName, 'internal', `Invalid field for action: ${field}`);
       this.field = field;
       this.type = typeName;
+    } else if (op === 'set-split-amount') {
+      this.field = null;
+      this.type = 'number';
     } else if (op === 'link-schedule') {
       this.field = null;
       this.type = 'id';
@@ -458,6 +467,14 @@ export class Action {
       case 'set':
         object[this.field] = this.value;
         break;
+      case 'set-split-amount':
+        switch (this.options.method) {
+          case 'fixed-amount':
+            object.amount = this.value;
+            break;
+          default:
+        }
+        break;
       case 'link-schedule':
         object.schedule = this.value;
         break;
@@ -476,6 +493,142 @@ export class Action {
   }
 }
 
+function execNonSplitActions(actions: Action[], transaction) {
+  const update = transaction;
+  actions.forEach(action => action.exec(update));
+  return update;
+}
+
+export function execActions(actions: Action[], transaction) {
+  const parentActions = actions.filter(action => !action.options?.splitIndex);
+  const childActions = actions.filter(action => action.options?.splitIndex);
+  const totalSplitCount =
+    actions.reduce(
+      (prev, cur) => Math.max(prev, cur.options?.splitIndex ?? 0),
+      0,
+    ) + 1;
+
+  let update = execNonSplitActions(parentActions, transaction);
+  if (!prefs.getPrefs()?.['flags.splitsInRules'] || totalSplitCount === 1) {
+    return update;
+  }
+
+  if (update.is_child) {
+    // Rules with splits can't be applied to child transactions.
+    return update;
+  }
+
+  const splitAmountActions = childActions.filter(
+    action => action.op === 'set-split-amount',
+  );
+  const fixedSplitAmountActions = splitAmountActions.filter(
+    action => action.options.method === 'fixed-amount',
+  );
+  const fixedAmountsBySplit: Record<number, number> = {};
+  fixedSplitAmountActions.forEach(action => {
+    const splitIndex = action.options.splitIndex ?? 0;
+    fixedAmountsBySplit[splitIndex] = action.value;
+  });
+  const fixedAmountSplitCount = Object.keys(fixedAmountsBySplit).length;
+  const totalFixedAmount = Object.values(fixedAmountsBySplit).reduce<number>(
+    (prev, cur: number) => prev + cur,
+    0,
+  );
+  if (
+    fixedAmountSplitCount === totalSplitCount &&
+    totalFixedAmount !== (transaction.amount ?? totalFixedAmount)
+  ) {
+    // Not all value would be distributed to a split.
+    return transaction;
+  }
+
+  const { data, newTransaction } = splitTransaction(
+    ungroupTransaction(update),
+    transaction.id,
+  );
+  update = recalculateSplit(newTransaction);
+  data[0] = update;
+  let newTransactions = data;
+
+  for (const action of childActions) {
+    const splitIndex = action.options?.splitIndex ?? 0;
+    if (splitIndex >= update.subtransactions.length) {
+      const { data, newTransaction } = addSplitTransaction(
+        newTransactions,
+        transaction.id,
+      );
+      update = recalculateSplit(newTransaction);
+      data[0] = update;
+      newTransactions = data;
+    }
+    action.exec(update.subtransactions[splitIndex]);
+  }
+
+  // Make sure every transaction has an amount.
+  if (fixedAmountSplitCount !== totalSplitCount) {
+    // This is the amount that will be distributed to the splits that
+    // don't have a fixed amount. The last split will get the remainder.
+    // The amount will be zero if the parent transaction has no amount.
+    const amountToDistribute =
+      (transaction.amount ?? totalFixedAmount) - totalFixedAmount;
+    let remainingAmount = amountToDistribute;
+
+    // First distribute the fixed percentages.
+    splitAmountActions
+      .filter(action => action.options.method === 'fixed-percent')
+      .forEach(action => {
+        const splitIndex = action.options.splitIndex;
+        const percent = action.value / 100;
+        const amount = Math.round(amountToDistribute * percent);
+        update.subtransactions[splitIndex].amount = amount;
+        remainingAmount -= amount;
+      });
+
+    // Then distribute the remainder.
+    const remainderSplitAmountActions = splitAmountActions.filter(
+      action => action.options.method === 'remainder',
+    );
+
+    // Check if there is any value left to distribute after all fixed
+    // (percentage and amount) splits have been distributed.
+    if (remainingAmount !== 0) {
+      // If there are no remainder splits explicitly added by the user,
+      // distribute the remainder to a virtual split that will be
+      // adjusted for the remainder.
+      if (remainderSplitAmountActions.length === 0) {
+        const splitIndex = totalSplitCount;
+        const { newTransaction } = addSplitTransaction(
+          newTransactions,
+          transaction.id,
+        );
+        update = recalculateSplit(newTransaction);
+        update.subtransactions[splitIndex].amount = remainingAmount;
+      } else {
+        const amountPerRemainderSplit = Math.round(
+          remainingAmount / remainderSplitAmountActions.length,
+        );
+        let lastNonFixedIndex = -1;
+        remainderSplitAmountActions.forEach(action => {
+          const splitIndex = action.options.splitIndex;
+          update.subtransactions[splitIndex].amount = amountPerRemainderSplit;
+          remainingAmount -= amountPerRemainderSplit;
+          lastNonFixedIndex = Math.max(lastNonFixedIndex, splitIndex);
+        });
+
+        // The last non-fixed split will be adjusted for the remainder.
+        update.subtransactions[lastNonFixedIndex].amount -= remainingAmount;
+      }
+      update = recalculateSplit(update);
+    }
+  }
+
+  // The split index 0 is reserved for "Before split" actions.
+  // Remove that entry from the subtransactions.
+  update.subtransactions = update.subtransactions.slice(1);
+
+  return update;
+}
+
 export class Rule {
   actions;
   conditions;
@@ -520,15 +673,23 @@ export class Rule {
     });
   }
 
-  execActions() {
-    const changes = {};
-    this.actions.forEach(action => action.exec(changes));
+  execActions(object) {
+    const result = execActions(this.actions, {
+      ...object,
+      subtransactions: object.subtransactions,
+    });
+    const changes = Object.keys(result).reduce((prev, cur) => {
+      if (result[cur] !== object[cur]) {
+        prev[cur] = result[cur];
+      }
+      return prev;
+    }, {});
     return changes;
   }
 
   exec(object) {
     if (this.evalConditions(object)) {
-      return this.execActions();
+      return this.execActions(object);
     }
     return null;
   }
diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts
index ccf2d09aba7818e9d83fc311c827554f187f5aea..5a3ccfba8bc181fc751495b685a6376ab254142f 100644
--- a/packages/loot-core/src/server/accounts/sync.ts
+++ b/packages/loot-core/src/server/accounts/sync.ts
@@ -487,7 +487,7 @@ export async function reconcileExternalTransactions(acctId, transactions) {
     transactionsStep1.push({
       payee_name,
       trans,
-      subtransactions,
+      subtransactions: trans.subtransactions || subtransactions,
       match,
       fuzzyDataset,
     });
@@ -650,7 +650,7 @@ export async function reconcileTransactions(acctId, transactions) {
     transactionsStep1.push({
       payee_name,
       trans,
-      subtransactions,
+      subtransactions: trans.subtransactions || subtransactions,
       match,
       fuzzyDataset,
     });
@@ -783,8 +783,12 @@ export async function addTransactions(
     };
 
     // Add split transactions if they are given
-    if (subtransactions && subtransactions.length > 0) {
-      added.push(...makeSplitTransaction(finalTransaction, subtransactions));
+    const updatedSubtransactions =
+      finalTransaction.subtransactions || subtransactions;
+    if (updatedSubtransactions && updatedSubtransactions.length > 0) {
+      added.push(
+        ...makeSplitTransaction(finalTransaction, updatedSubtransactions),
+      );
     } else {
       added.push(finalTransaction);
     }
diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/accounts/transaction-rules.ts
index a8afebff64fee25557936d1a379c150dee1bfbd8..926b6797cabe6d14be3c85fe60a68f24a3e196b2 100644
--- a/packages/loot-core/src/server/accounts/transaction-rules.ts
+++ b/packages/loot-core/src/server/accounts/transaction-rules.ts
@@ -11,6 +11,7 @@ import {
   sortNumbers,
   getApproxNumberThreshold,
 } from '../../shared/rules';
+import { ungroupTransaction } from '../../shared/transactions';
 import { partitionByField, fastSetMerge } from '../../shared/util';
 import {
   type TransactionEntity,
@@ -32,6 +33,7 @@ import {
   rankRules,
   migrateIds,
   iterateIds,
+  execActions,
 } from './rules';
 import { batchUpdateTransactions } from './transactions';
 
@@ -492,8 +494,8 @@ export function conditionsToAQL(conditions, { recurDateBounds = 100 } = {}) {
   return { filters, errors };
 }
 
-export function applyActions(
-  transactionIds: string[],
+export async function applyActions(
+  transactions: TransactionEntity[],
   actions: Array<Action | RuleActionEntity>,
 ) {
   const parsedActions = actions
@@ -526,12 +528,8 @@ export function applyActions(
     return null;
   }
 
-  const updated = transactionIds.map(id => {
-    const update = { id };
-    for (const action of parsedActions) {
-      action.exec(update);
-    }
-    return update;
+  const updated = transactions.flatMap(trans => {
+    return ungroupTransaction(execActions(parsedActions, trans));
   });
 
   return batchUpdateTransactions({ updated });
diff --git a/packages/loot-core/src/server/rules/app.ts b/packages/loot-core/src/server/rules/app.ts
index 62dd517d4dd022fcca57e33bdf9f4af3bf42d565..8fb059c5a94ef60942a7fd079a80adb9b9ed17de 100644
--- a/packages/loot-core/src/server/rules/app.ts
+++ b/packages/loot-core/src/server/rules/app.ts
@@ -129,8 +129,8 @@ app.method(
 app.method(
   'rule-apply-actions',
   mutator(
-    undoable(async function ({ transactionIds, actions }) {
-      return rules.applyActions(transactionIds, actions);
+    undoable(async function ({ transactions, actions }) {
+      return rules.applyActions(transactions, actions);
     }),
   ),
 );
diff --git a/packages/loot-core/src/server/rules/types/handlers.ts b/packages/loot-core/src/server/rules/types/handlers.ts
index 95dfce095b8cc8de254dbd386f484622a946da8d..429acddc91dc30750e04b5401acbb856e1d09300 100644
--- a/packages/loot-core/src/server/rules/types/handlers.ts
+++ b/packages/loot-core/src/server/rules/types/handlers.ts
@@ -31,7 +31,7 @@ export interface RulesHandlers {
   ) => Promise<{ someDeletionsFailed: boolean }>;
 
   'rule-apply-actions': (arg: {
-    transactionIds: string[];
+    transactions: TransactionEntity[];
     actions: Array<Action | RuleActionEntity>;
   }) => Promise<null | { added: TransactionEntity[]; updated: unknown[] }>;
 
diff --git a/packages/loot-core/src/shared/rules.ts b/packages/loot-core/src/shared/rules.ts
index b1e40f373fa661c37121980efcd3f83b75f502d9..9eb16dade64b75ce30bd46f02e9607b9d7e820a6 100644
--- a/packages/loot-core/src/shared/rules.ts
+++ b/packages/loot-core/src/shared/rules.ts
@@ -159,6 +159,13 @@ export function sortNumbers(num1, num2) {
 }
 
 export function parse(item) {
+  if (item.op === 'set-split-amount') {
+    if (item.options.method === 'fixed-amount') {
+      return { ...item, value: item.value && integerToAmount(item.value) };
+    }
+    return item;
+  }
+
   switch (item.type) {
     case 'number': {
       let parsed = item.value;
@@ -186,6 +193,22 @@ export function parse(item) {
 }
 
 export function unparse({ error, inputKey, ...item }) {
+  if (item.op === 'set-split-amount') {
+    if (item.options.method === 'fixed-amount') {
+      return {
+        ...item,
+        value: item.value && amountToInteger(item.value),
+      };
+    }
+    if (item.options.method === 'fixed-percent') {
+      return {
+        ...item,
+        value: item.value && parseFloat(item.value),
+      };
+    }
+    return item;
+  }
+
   switch (item.type) {
     case 'number': {
       let unparsed = item.value;
diff --git a/packages/loot-core/src/shared/schedules.ts b/packages/loot-core/src/shared/schedules.ts
index d8628ddfa0d320391836442114a5f1337662b5a5..174b8102c39ac50b6585fb048a942ccc32d56188 100644
--- a/packages/loot-core/src/shared/schedules.ts
+++ b/packages/loot-core/src/shared/schedules.ts
@@ -43,9 +43,9 @@ export function getHasTransactionsQuery(schedules) {
   });
 
   return q('transactions')
+    .options({ splits: 'grouped' })
     .filter({ $or: filters })
     .orderBy({ date: 'desc' })
-    .groupBy('schedule')
     .select(['schedule', 'date']);
 }
 
diff --git a/packages/loot-core/src/shared/transactions.ts b/packages/loot-core/src/shared/transactions.ts
index b642454e4e78f15da45c25f11b1a7557ec73fdeb..532047a223c5f71218a66ec61e58e80c446aecaf 100644
--- a/packages/loot-core/src/shared/transactions.ts
+++ b/packages/loot-core/src/shared/transactions.ts
@@ -97,7 +97,7 @@ export function ungroupTransactions(transactions: TransactionEntity[]) {
   }, []);
 }
 
-function groupTransaction(split) {
+export function groupTransaction(split) {
   return { ...split[0], subtransactions: split.slice(1) };
 }
 
diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts
index ce2b384f66592b89f0b8ca47ae7bec8d4c65e6be..66fa53f99a24de57afe9dd8aa8510ceb07af0ca8 100644
--- a/packages/loot-core/src/types/models/rule.d.ts
+++ b/packages/loot-core/src/types/models/rule.d.ts
@@ -46,10 +46,21 @@ export interface SetRuleActionEntity {
   field: string;
   op: 'set';
   value: unknown;
-  options?: unknown;
+  options?: {
+    splitIndex?: number;
+  };
   type?: string;
 }
 
+export interface SetSplitAmountRuleActionEntity {
+  op: 'set-split-amount';
+  value: number;
+  options?: {
+    splitIndex?: number;
+    method: 'fixed-amount' | 'fixed-percent' | 'remainder';
+  };
+}
+
 export interface LinkScheduleRuleActionEntity {
   op: 'link-schedule';
   value: ScheduleEntity;
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index f483d672ef906475a96f17d7b221bb443154c281..e924b329013e1796759d0f02fc100e12bf647e25 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -6,7 +6,8 @@ export type FeatureFlag =
   | 'reportBudget'
   | 'goalTemplatesEnabled'
   | 'customReports'
-  | 'simpleFinSync';
+  | 'simpleFinSync'
+  | 'splitsInRules';
 
 export type LocalPrefs = Partial<
   {
diff --git a/upcoming-release-notes/2059.md b/upcoming-release-notes/2059.md
new file mode 100644
index 0000000000000000000000000000000000000000..1518c16d6129c1f49c5a3e36e7539fa993f4f06f
--- /dev/null
+++ b/upcoming-release-notes/2059.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [jfdoming]
+---
+
+Support automatically splitting transactions with rules