diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 3c5bdb4b1be7..4ee9d947ec86 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -850,6 +850,7 @@ const CONST = {
         PERSONAL_CARD_IMPORT: 'personalCardImport',
         SUGGESTED_FOLLOWUPS: 'suggestedFollowups',
         FREEZE_CARD: 'freezeCard',
+        BULK_EDIT: 'bulkEdit',
         NEW_MANUAL_EXPENSE_FLOW: 'newManualExpenseFlow',
     },
     BUTTON_STATES: {
@@ -3248,6 +3249,8 @@ const CONST = {
         QUANTITY_MAX_LENGTH: 12,
         // This is the transactionID used when going through the create expense flow so that it mimics a real transaction (like the edit flow)
         OPTIMISTIC_TRANSACTION_ID: '1',
+        // This is the transactionID used when bulk editing multiple expenses
+        OPTIMISTIC_BULK_EDIT_TRANSACTION_ID: 'optimisticBulkEditTransactionID',
         // This is the transactionID used when going through the distance split expense flow so that it mimics a draft transaction
         OPTIMISTIC_DISTANCE_SPLIT_TRANSACTION_ID: '2',
         // Note: These payment types are used when building IOU reportAction message values in the server and should
@@ -4556,6 +4559,7 @@ const CONST = {
         TAX_RATE: 'taxRate',
         TAX_AMOUNT: 'taxAmount',
         REIMBURSABLE: 'reimbursable',
+        BILLABLE: 'billable',
         REPORT: 'report',
     },
     FOOTER: {
@@ -7357,6 +7361,7 @@ const CONST = {
             TAG: 'tag',
         },
         BULK_ACTION_TYPES: {
+            EDIT: 'edit',
             EXPORT: 'export',
             APPROVE: 'approve',
             PAY: 'pay',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 6f1a861e3cc9..63544894ebd5 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -1047,6 +1047,12 @@ const ONYXKEYS = {
         WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft',
         ENABLE_GLOBAL_REIMBURSEMENTS: 'enableGlobalReimbursementsForm',
         ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT: 'enableGlobalReimbursementsFormDraft',
+        SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM: 'searchEditMultipleDescriptionForm',
+        SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM_DRAFT: 'searchEditMultipleDescriptionFormDraft',
+        SEARCH_EDIT_MULTIPLE_MERCHANT_FORM: 'searchEditMultipleMerchantForm',
+        SEARCH_EDIT_MULTIPLE_MERCHANT_FORM_DRAFT: 'searchEditMultipleMerchantFormDraft',
+        SEARCH_EDIT_MULTIPLE_DATE_FORM: 'searchEditMultipleDateForm',
+        SEARCH_EDIT_MULTIPLE_DATE_FORM_DRAFT: 'searchEditMultipleDateFormDraft',
         CREATE_DOMAIN_FORM: 'createDomainForm',
         CREATE_DOMAIN_FORM_DRAFT: 'createDomainFormDraft',
         SPLIT_EXPENSE_EDIT_DATES: 'splitExpenseEditDates',
@@ -1187,6 +1193,9 @@ type OnyxFormValuesMapping = {
     [ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm;
     [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm;
     [ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS]: FormTypes.EnableGlobalReimbursementsForm;
+    [ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM]: FormTypes.SearchEditMultipleDescriptionForm;
+    [ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM]: FormTypes.SearchEditMultipleMerchantForm;
+    [ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM]: FormTypes.SearchEditMultipleDateForm;
     [ONYXKEYS.FORMS.CREATE_DOMAIN_FORM]: FormTypes.CreateDomainForm;
     [ONYXKEYS.FORMS.SPLIT_EXPENSE_EDIT_DATES]: FormTypes.SplitExpenseEditDateForm;
     [ONYXKEYS.FORMS.EXPENSE_RULE_FORM]: FormTypes.ExpenseRuleForm;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index a93593e4d813..e4ad270ed6d8 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -243,6 +243,19 @@ const ROUTES = {
         },
     },
     SEARCH_REJECT_REASON_RHP: 'search/reject',
+    SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP: 'search/edit-multiple-transactions',
+    SEARCH_EDIT_MULTIPLE_AMOUNT_RHP: 'search/edit-multiple/amount',
+    SEARCH_EDIT_MULTIPLE_DESCRIPTION_RHP: 'search/edit-multiple/description',
+    SEARCH_EDIT_MULTIPLE_MERCHANT_RHP: 'search/edit-multiple/merchant',
+    SEARCH_EDIT_MULTIPLE_DATE_RHP: 'search/edit-multiple/date',
+    SEARCH_EDIT_MULTIPLE_CATEGORY_RHP: 'search/edit-multiple/category',
+    SEARCH_EDIT_MULTIPLE_TAG_RHP: {
+        route: 'search/edit-multiple/tag/:tagListIndex',
+        getRoute: (tagListIndex = 0) => `search/edit-multiple/tag/${tagListIndex}` as const,
+    },
+    SEARCH_EDIT_MULTIPLE_BILLABLE_RHP: 'search/edit-multiple/billable',
+    SEARCH_EDIT_MULTIPLE_REIMBURSABLE_RHP: 'search/edit-multiple/reimbursable',
+    SEARCH_EDIT_MULTIPLE_TAX_RHP: 'search/edit-multiple/tax',
     MOVE_TRANSACTIONS_SEARCH_RHP: {
         route: 'search/move-transactions/search/:backTo?',
         getRoute: (backTo?: string) => {
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 2f9f07fed9e3..f4e989a0b943 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -98,6 +98,16 @@ const SCREENS = {
         TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
         TRANSACTION_HOLD_REASON_SEARCH: 'Search_Transaction_Hold_Reason_Search',
         SEARCH_REJECT_REASON_RHP: 'Search_Reject_Reason_RHP',
+        EDIT_MULTIPLE_TRANSACTIONS_RHP: 'Search_Edit_Multiple_Transactions_RHP',
+        EDIT_MULTIPLE_AMOUNT_RHP: 'Search_Edit_Multiple_Amount_RHP',
+        EDIT_MULTIPLE_DESCRIPTION_RHP: 'Search_Edit_Multiple_Description_RHP',
+        EDIT_MULTIPLE_MERCHANT_RHP: 'Search_Edit_Multiple_Merchant_RHP',
+        EDIT_MULTIPLE_DATE_RHP: 'Search_Edit_Multiple_Date_RHP',
+        EDIT_MULTIPLE_CATEGORY_RHP: 'Search_Edit_Multiple_Category_RHP',
+        EDIT_MULTIPLE_TAG_RHP: 'Search_Edit_Multiple_Tag_RHP',
+        EDIT_MULTIPLE_BILLABLE_RHP: 'Search_Edit_Multiple_Billable_RHP',
+        EDIT_MULTIPLE_REIMBURSABLE_RHP: 'Search_Edit_Multiple_Reimbursable_RHP',
+        EDIT_MULTIPLE_TAX_RHP: 'Search_Edit_Multiple_Tax_RHP',
         TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP: 'Search_Transactions_Change_Report_Search',
     },
     SETTINGS: {
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 1dc34d4459d4..2625d923429d 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -77,7 +77,7 @@ import {
     isTrackExpenseReportNew,
     shouldEnableNegative,
 } from '@libs/ReportUtils';
-import {hasEnabledTags} from '@libs/TagsOptionsListUtils';
+import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils';
 import {
     getBillable,
     getCurrency,
@@ -87,7 +87,6 @@ import {
     getOriginalAmountForDisplay,
     getOriginalTransactionWithSplitInfo,
     getReimbursable,
-    getTagArrayFromName,
     getTagForDisplay,
     getTaxName,
     hasMissingSmartscanFields,
@@ -738,30 +737,7 @@ function MoneyRequestView({
         const tagForDisplay = getTagForDisplay(updatedTransaction ?? transaction, index);
         let shouldShow = false;
         if (hasDependentTags) {
-            if (index === 0) {
-                shouldShow = true;
-            } else {
-                const prevTagValue = getTagForDisplay(transaction, index - 1);
-                if (!prevTagValue) {
-                    shouldShow = false;
-                } else {
-                    const parentTag = getTagArrayFromName(transactionTag ?? '')
-                        .slice(0, index)
-                        .join(':');
-
-                    const availableTags = Object.values(tags).filter((policyTag) => {
-                        const filterRegex = policyTag.rules?.parentTagsFilter;
-                        if (!filterRegex) {
-                            return true;
-                        }
-
-                        const regex = new RegExp(filterRegex);
-                        return regex.test(parentTag ?? '');
-                    });
-
-                    shouldShow = availableTags.some((tag) => tag.enabled);
-                }
-            }
+            shouldShow = shouldShowDependentTagList(index, transactionTag, tags);
         } else {
             shouldShow = !!tagForDisplay || (canEdit && hasEnabledOptions(tags));
         }
diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx
index 8e7b6e800f17..ce898c868b9c 100644
--- a/src/components/TaxPicker.tsx
+++ b/src/components/TaxPicker.tsx
@@ -40,9 +40,25 @@ type TaxPickerProps = {
      * If enabled, the content will have a bottom padding equal to account for the safe bottom area inset.
      */
     addBottomSafeAreaPadding?: boolean;
+
+    /**
+     * If enabled, allows deselecting the currently selected tax rate by tapping it again.
+     * When disabled (default), tapping the selected tax rate will dismiss the picker without calling onSubmit.
+     */
+    allowDeselect?: boolean;
 };
 
-function TaxPicker({selectedTaxRate = '', policyID, transactionID, onSubmit, action, iouType, onDismiss = Navigation.goBack, addBottomSafeAreaPadding}: TaxPickerProps) {
+function TaxPicker({
+    selectedTaxRate = '',
+    policyID,
+    transactionID,
+    onSubmit,
+    action,
+    iouType,
+    onDismiss = Navigation.goBack,
+    addBottomSafeAreaPadding,
+    allowDeselect = false,
+}: TaxPickerProps) {
     const {translate, localeCompare} = useLocalize();
     const [searchValue, setSearchValue] = useState('');
     const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`);
@@ -119,7 +135,8 @@ function TaxPicker({selectedTaxRate = '', policyID, transactionID, onSubmit, act
         const currentTaxRateValue = taxCode ? taxRates?.taxes?.[taxCode]?.value : undefined;
         const hasMatchingTaxValue = taxValue === undefined || currentTaxRateValue === taxValue;
 
-        if (isSameTaxCode && hasMatchingTaxValue) {
+        // If deselection is not allowed and the same option is selected, just dismiss
+        if (!allowDeselect && isSameTaxCode && hasMatchingTaxValue) {
             onDismiss();
             return;
         }
diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts
index c1b5970f2482..0fef9201cbba 100644
--- a/src/hooks/useSearchBulkActions.ts
+++ b/src/hooks/useSearchBulkActions.ts
@@ -37,6 +37,7 @@ import Navigation from '@libs/Navigation/Navigation';
 import {getConnectedIntegration} from '@libs/PolicyUtils';
 import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils';
 import {
+    canEditMultipleTransactions,
     getIntegrationIcon,
     getReportOrDraftReport,
     isBusinessInvoiceRoom,
@@ -49,7 +50,7 @@ import {navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils';
 import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
 import {hasTransactionBeenRejected, isDeletedTransaction} from '@libs/TransactionUtils';
 import variables from '@styles/variables';
-import {canIOUBePaid, dismissRejectUseExplanation} from '@userActions/IOU';
+import {canIOUBePaid, dismissRejectUseExplanation, initBulkEditDraftTransaction} from '@userActions/IOU';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
@@ -63,6 +64,7 @@ import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
 import useLocalize from './useLocalize';
 import useNetwork from './useNetwork';
 import useOnyx from './useOnyx';
+import usePermissions from './usePermissions';
 import usePersonalPolicy from './usePersonalPolicy';
 import useSelfDMReport from './useSelfDMReport';
 import useTheme from './useTheme';
@@ -98,6 +100,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
     const {accountID} = currentUserPersonalDetails;
     const allTransactions = useAllTransactions();
     const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+    const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
     const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
     const selfDMReport = useSelfDMReport();
     const [lastPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD);
@@ -110,6 +113,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
     const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
     const personalPolicy = usePersonalPolicy();
     const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
+    const {isBetaEnabled} = usePermissions();
     const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
     const undeleteTransactions = useUndeleteTransactions();
 
@@ -671,7 +675,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
     );
 
     const onBulkPaySelectedRef = useRef(onBulkPaySelected);
-    onBulkPaySelectedRef.current = onBulkPaySelected;
+    useEffect(() => {
+        onBulkPaySelectedRef.current = onBulkPaySelected;
+    });
     const stableOnBulkPaySelected = useCallback((paymentMethod?: PaymentMethodType, additionalData?: BulkPaySelectionData) => {
         onBulkPaySelectedRef.current?.(paymentMethod, additionalData);
     }, []);
@@ -886,6 +892,30 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
             return [exportButtonOption];
         }
 
+        const isExpenseReportSearch = typeExpenseReport || searchResults?.search.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
+        const selectedTransactionsList = Object.values(selectedTransactions)
+            .map((transaction) => transaction.transaction)
+            .filter((transaction): transaction is Transaction => !!transaction);
+        const canEditMultiple =
+            canEditMultipleTransactions(selectedTransactionsList, allReportActions, allReports, policies, isExpenseReportSearch, searchResults?.data) && isBetaEnabled(CONST.BETAS.BULK_EDIT);
+
+        if (canEditMultiple) {
+            options.push({
+                icon: expensifyIcons.Pencil,
+                text: translate('search.bulkActions.editMultiple'),
+                value: CONST.SEARCH.BULK_ACTION_TYPES.EDIT,
+                shouldCloseModalOnSelect: true,
+                onSelected: () => {
+                    const selectedTransactionIDs = Object.keys(selectedTransactions).filter((transactionID) => {
+                        const selectedTransaction = selectedTransactions[transactionID];
+                        return !!selectedTransaction?.transaction?.transactionID || !!allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+                    });
+                    initBulkEditDraftTransaction(selectedTransactionIDs);
+                    Navigation.navigate(ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP);
+                },
+            });
+        }
+
         const areSelectedTransactionsIncludedInReports = selectedTransactionsKeys.every((id) =>
             selectedTransactions[id].reportID ? selectedReportIDs.includes(selectedTransactions[id].reportID) : true,
         );
@@ -1190,6 +1220,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
         expensifyIcons.ArrowCollapse,
         expensifyIcons.DocumentMerge,
         expensifyIcons.ArrowSplit,
+        expensifyIcons.Pencil,
         expensifyIcons.Trashcan,
         expensifyIcons.RotateLeft,
         expensifyIcons.Exclamation,
@@ -1205,6 +1236,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
         selectedTransactionReportIDs,
         selectedPolicyIDs,
         policies,
+        allReportActions,
         integrationsExportTemplates,
         csvExportLayouts,
         allReports,
@@ -1242,6 +1274,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
         userBillingGraceEndPeriods,
         ownerBillingGraceEndPeriod,
         currentSearchKey,
+        allTransactions,
+        isBetaEnabled,
+        shouldShowBusinessBankAccountOptions,
     ]);
 
     const handleOfflineModalClose = useCallback(() => {
diff --git a/src/hooks/useSearchBulkEditPolicyID.ts b/src/hooks/useSearchBulkEditPolicyID.ts
new file mode 100644
index 000000000000..8c112083cdca
--- /dev/null
+++ b/src/hooks/useSearchBulkEditPolicyID.ts
@@ -0,0 +1,29 @@
+import {useSearchStateContext} from '@components/Search/SearchContext';
+import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils';
+import {withSnapshotReports, withSnapshotTransactions} from '@pages/Search/SearchEditMultiple/SearchEditMultipleUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useOnyx from './useOnyx';
+
+/**
+ * Resolves the bulk-edit policyID from the selected transactions, using
+ * snapshot-merged collections so that expenses only present in the search
+ * snapshot (e.g. after a hard refresh) are still resolved correctly.
+ */
+function useSearchBulkEditPolicyID(): string | undefined {
+    const {currentSearchResults} = useSearchStateContext();
+    const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+    const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
+    const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+
+    const snapshotData = currentSearchResults?.data;
+    const mergedTransactions = withSnapshotTransactions(allTransactions, snapshotData);
+    const mergedReports = withSnapshotReports(allReports, snapshotData);
+
+    const selectedTransactionIDs = draftTransaction?.selectedTransactionIDs ?? [];
+
+    return getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, mergedTransactions, mergedReports);
+}
+
+export default useSearchBulkEditPolicyID;
diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts
index a3acb09805fa..725ef47c0038 100644
--- a/src/hooks/useSelectedTransactionsActions.ts
+++ b/src/hooks/useSelectedTransactionsActions.ts
@@ -4,6 +4,7 @@ import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
 import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
 import type {PopoverMenuItem} from '@components/PopoverMenu';
 import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
+import {initBulkEditDraftTransaction} from '@libs/actions/IOU';
 import {unholdRequest} from '@libs/actions/IOU/Hold';
 import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction';
 import {exportReportToCSV} from '@libs/actions/Report';
@@ -16,6 +17,7 @@ import {
     canDeleteCardTransactionByLiabilityType,
     canDeleteTransaction,
     canEditFieldOfMoneyRequest,
+    canEditMultipleTransactions,
     canHoldUnholdReportAction,
     canRejectReportAction,
     canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
@@ -40,6 +42,7 @@ import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
 import useLocalize from './useLocalize';
 import useNetworkWithOfflineStatus from './useNetworkWithOfflineStatus';
 import useOnyx from './useOnyx';
+import usePermissions from './usePermissions';
 import useReportIsArchived from './useReportIsArchived';
 
 // We do not use PRIMARY_REPORT_ACTIONS or SECONDARY_REPORT_ACTIONS because they weren't meant to be used in this situation. `value` property of returned options is later ignored.
@@ -78,15 +81,31 @@ function useSelectedTransactionsActions({
     const {selectedTransactionIDs, currentSearchHash, selectedTransactions: selectedTransactionsMeta} = useSearchStateContext();
     const {clearSelectedTransactions} = useSearchActionsContext();
     const allTransactions = useAllTransactions();
+    const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+    const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
+    const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
     const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
     const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH);
     const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES);
     const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS);
     const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
 
-    const expensifyIcons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'Trashcan', 'ArrowRight', 'Table', 'DocumentMerge', 'Export', 'ArrowCollapse', 'ArrowSplit', 'ThumbsDown']);
+    const expensifyIcons = useMemoizedLazyExpensifyIcons([
+        'Stopwatch',
+        'Trashcan',
+        'ArrowRight',
+        'Table',
+        'DocumentMerge',
+        'Export',
+        'ArrowCollapse',
+        'ArrowSplit',
+        'ThumbsDown',
+        'Pencil',
+    ] as const);
+
     const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs);
     const isReportArchived = useReportIsArchived(report?.reportID);
+    const {isBetaEnabled} = usePermissions();
     const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy});
     const {login, accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
     const selectedTransactionsList = selectedTransactionIDs.reduce((acc, transactionID) => {
@@ -186,6 +205,20 @@ function useSelectedTransactionsActions({
     let computedOptions: Array<DropdownOption<string>> = [];
     if (selectedTransactionIDs.length) {
         const options = [];
+
+        const canEditMultiple = canEditMultipleTransactions(selectedTransactionsList, allReportActions, allReports, allPolicies) && isBetaEnabled(CONST.BETAS.BULK_EDIT);
+
+        if (canEditMultiple) {
+            options.push({
+                text: translate('search.bulkActions.editMultiple'),
+                icon: expensifyIcons.Pencil,
+                value: CONST.SEARCH.BULK_ACTION_TYPES.EDIT,
+                onSelected: () => {
+                    initBulkEditDraftTransaction(selectedTransactionIDs);
+                    Navigation.navigate(ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP);
+                },
+            });
+        }
         const isMoneyRequestReport = isMoneyRequestReportUtils(report);
         const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
 
@@ -412,6 +445,7 @@ function useSelectedTransactionsActions({
                 },
             });
         }
+
         computedOptions = options;
     }
 
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 7d46462206c2..ab0adfbaa4d9 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -7346,6 +7346,9 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und
         topMerchants: 'Top-Händler',
         groupedExpenses: 'gruppierte Ausgaben',
         bulkActions: {
+            editMultiple: 'Mehrere bearbeiten',
+            editMultipleTitle: 'Mehrere Ausgaben bearbeiten',
+            editMultipleDescription: 'Änderungen werden für alle ausgewählten Ausgaben festgelegt und überschreiben alle zuvor festgelegten Werte.',
             approve: 'Genehmigen',
             pay: 'Bezahlen',
             delete: 'Löschen',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index a9a620fdc593..18a5d044b3be 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7342,6 +7342,10 @@ const translations = {
         spendOverTime: 'Spend over time',
         groupedExpenses: 'grouped expenses',
         bulkActions: {
+            editMultiple: 'Edit multiple',
+            editMultipleTitle: 'Edit multiple expenses',
+            // cspell:disable
+            editMultipleDescription: "Changes will be set for all selected expenses and will override any previously set values. Just sayin'.",
             approve: 'Approve',
             pay: 'Pay',
             delete: 'Delete',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 5f0a8e211129..073133b087fd 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -7245,6 +7245,9 @@ ${amount} para ${merchant} - ${date}`,
         deleteSavedSearchConfirm: '¿Estás seguro de que quieres eliminar esta búsqueda?',
         groupedExpenses: 'gastos agrupados',
         bulkActions: {
+            editMultiple: 'Editar múltiples',
+            editMultipleTitle: 'Editar múltiples gastos',
+            editMultipleDescription: 'Los cambios se aplicarán a todos los gastos seleccionados y anularán cualquier valor previamente establecido.',
             approve: 'Aprobar',
             pay: 'Pagar',
             delete: 'Eliminar',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 5cb6ed391df5..a9c40a417604 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7370,6 +7370,9 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip
         topMerchants: 'Commerçants principaux',
         groupedExpenses: 'dépenses groupées',
         bulkActions: {
+            editMultiple: 'Modifier plusieurs',
+            editMultipleTitle: 'Modifier plusieurs dépenses',
+            editMultipleDescription: 'Les modifications seront appliquées à toutes les dépenses sélectionnées et remplaceront toutes les valeurs précédemment définies.',
             approve: 'Approuver',
             pay: 'Payer',
             delete: 'Supprimer',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 2eaf50b46502..8cc03982939e 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -7334,6 +7334,9 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo
         topMerchants: 'Commercianti principali',
         groupedExpenses: 'spese raggruppate',
         bulkActions: {
+            editMultiple: 'Modifica multipli',
+            editMultipleTitle: 'Modifica più spese',
+            editMultipleDescription: 'Le modifiche verranno applicate a tutte le spese selezionate e sostituiranno i valori precedentemente impostati.',
             approve: 'Approva',
             pay: 'Paga',
             delete: 'Elimina',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 6395dccd9dda..b0470bf7968b 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7248,6 +7248,9 @@ ${reportName}
         topMerchants: '上位加盟店',
         groupedExpenses: 'グループ化された経費',
         bulkActions: {
+            editMultiple: '複数を編集',
+            editMultipleTitle: '複数の経費を編集',
+            editMultipleDescription: '変更は選択したすべての経費に適用され、以前に設定された値は上書きされます。',
             approve: '承認',
             pay: '支払う',
             delete: '削除',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 2582ad6bb143..c9128518e31f 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -7312,6 +7312,9 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar
         topMerchants: 'Topverkopers',
         groupedExpenses: 'gegroepeerde uitgaven',
         bulkActions: {
+            editMultiple: 'Meerdere bewerken',
+            editMultipleTitle: 'Meerdere uitgaven bewerken',
+            editMultipleDescription: 'Wijzigingen worden toegepast op alle geselecteerde uitgaven en overschrijven eerder ingestelde waarden.',
             approve: 'Goedkeuren',
             pay: 'Betalen',
             delete: 'Verwijderen',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 3bcc856b9c75..df8dfcdc16bb 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -7301,6 +7301,9 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
         topMerchants: 'Najlepsi sprzedawcy',
         groupedExpenses: 'zgrupowane wydatki',
         bulkActions: {
+            editMultiple: 'Edytuj wiele',
+            editMultipleTitle: 'Edytuj wiele wydatków',
+            editMultipleDescription: 'Zmiany zostaną zastosowane do wszystkich wybranych wydatków i zastąpią wcześniej ustawione wartości.',
             approve: 'Zatwierdź',
             pay: 'Zapłać',
             delete: 'Usuń',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index d6595356844c..556692aa27b1 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -7304,6 +7304,9 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e
         topMerchants: 'Principais comerciantes',
         groupedExpenses: 'despesas agrupadas',
         bulkActions: {
+            editMultiple: 'Editar múltiplos',
+            editMultipleTitle: 'Editar múltiplas despesas',
+            editMultipleDescription: 'As alterações serão aplicadas a todas as despesas selecionadas e substituirão quaisquer valores definidos anteriormente.',
             approve: 'Aprovar',
             pay: 'Pagar',
             delete: 'Excluir',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index f7e2e47f4500..676f74aaa912 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -7124,6 +7124,9 @@ ${reportName}
         topMerchants: '热门商家',
         groupedExpenses: '已分组的报销费用',
         bulkActions: {
+            editMultiple: '批量编辑',
+            editMultipleTitle: '编辑多个费用',
+            editMultipleDescription: '更改将应用于所有选定的费用，并将覆盖之前设置的任何值。',
             approve: '批准',
             pay: '支付',
             delete: '删除',
diff --git a/src/libs/API/parameters/UpdateMoneyRequestParams.ts b/src/libs/API/parameters/UpdateMoneyRequestParams.ts
index 0f210d6c661d..68dbb7a8f679 100644
--- a/src/libs/API/parameters/UpdateMoneyRequestParams.ts
+++ b/src/libs/API/parameters/UpdateMoneyRequestParams.ts
@@ -4,6 +4,8 @@ type UpdateMoneyRequestParams = Partial<TransactionDetails> & {
     reportID?: string;
     transactionID?: string;
     reportActionID?: string;
+    /** Used for bulk updates - JSON stringified object containing only changed fields */
+    updates?: string;
 };
 
 export default UpdateMoneyRequestParams;
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 310b54a942c7..8f114b9a35e4 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -206,6 +206,7 @@ const WRITE_COMMANDS = {
     DELETE_REPORT_FIELD: 'RemoveReportField',
     SET_REPORT_NAME: 'RenameReport',
     COMPLETE_SPLIT_BILL: 'CompleteSplitBill',
+    UPDATE_MONEY_REQUEST: 'UpdateMoneyRequest',
     UPDATE_MONEY_REQUEST_ATTENDEES: 'UpdateMoneyRequestAttendees',
     UPDATE_MONEY_REQUEST_DATE: 'UpdateMoneyRequestDate',
     UPDATE_MONEY_REQUEST_REIMBURSABLE: 'UpdateMoneyRequestReimbursable',
@@ -766,6 +767,7 @@ type WriteCommandParameters = {
     [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams;
     [WRITE_COMMANDS.DELETE_REPORT_FIELD]: Parameters.DeleteReportFieldParams;
     [WRITE_COMMANDS.COMPLETE_SPLIT_BILL]: Parameters.CompleteSplitBillParams;
+    [WRITE_COMMANDS.UPDATE_MONEY_REQUEST]: Parameters.UpdateMoneyRequestParams;
     [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES]: Parameters.UpdateMoneyRequestParams;
     [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE]: Parameters.UpdateMoneyRequestParams;
     [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams;
diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts
index edd20ed68091..6455a9ee08c5 100644
--- a/src/libs/DebugUtils.ts
+++ b/src/libs/DebugUtils.ts
@@ -999,6 +999,8 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
             return validateNumber(value);
         case 'iouRequestType':
             return validateConstantEnum(value, CONST.IOU.REQUEST_TYPE);
+        case 'selectedTransactionIDs':
+            return validateArray(value, 'string');
         case 'participants':
             return validateArray<ArrayElement<Transaction, 'participants'>>(value, {
                 accountID: 'number',
@@ -1094,6 +1096,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
                     routes: CONST.RED_BRICK_ROAD_PENDING_ACTION,
                     routeDistanceMeters: CONST.RED_BRICK_ROAD_PENDING_ACTION,
                     transactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION,
+                    selectedTransactionIDs: CONST.RED_BRICK_ROAD_PENDING_ACTION,
                     tag: CONST.RED_BRICK_ROAD_PENDING_ACTION,
                     transactionType: CONST.RED_BRICK_ROAD_PENDING_ACTION,
                     isFromGlobalCreate: CONST.RED_BRICK_ROAD_PENDING_ACTION,
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 43f7871b74ba..6555935a32f2 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -991,6 +991,16 @@ const SearchReportActionsModalStackNavigator = createModalStackNavigator<SearchR
     [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_SEARCH]: () => require<ReactComponentModule>('../../../../pages/Search/SearchHoldReasonPage').default,
     [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchRejectReasonPage').default,
     [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchTransactionsChangeReport').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_TRANSACTIONS_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultiplePage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_AMOUNT_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_DESCRIPTION_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_MERCHANT_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_DATE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleDatePage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_CATEGORY_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_TAG_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleTagPage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_BILLABLE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_REIMBURSABLE_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage').default,
+    [SCREENS.SEARCH.EDIT_MULTIPLE_TAX_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchEditMultiple/SearchEditMultipleTaxPage').default,
 });
 
 const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator<SearchAdvancedFiltersParamList>({
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index d0998e2be621..ce4c91abbe33 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1912,6 +1912,16 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
                             exact: true,
                         },
                         [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: ROUTES.SEARCH_REJECT_REASON_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_TRANSACTIONS_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_AMOUNT_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_AMOUNT_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_DESCRIPTION_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_DESCRIPTION_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_MERCHANT_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_MERCHANT_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_DATE_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_DATE_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_CATEGORY_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_CATEGORY_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_TAG_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_TAG_RHP.route,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_BILLABLE_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_BILLABLE_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_REIMBURSABLE_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_REIMBURSABLE_RHP,
+                        [SCREENS.SEARCH.EDIT_MULTIPLE_TAX_RHP]: ROUTES.SEARCH_EDIT_MULTIPLE_TAX_RHP,
                         [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: {
                             path: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP.route,
                             exact: true,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index f26ca0601a91..723d54312ae0 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -55,6 +55,7 @@ import type {
     ReportMetadata,
     ReportNameValuePairs,
     ReportViolationName,
+    SearchResults,
     Task,
     Transaction,
     TransactionViolation,
@@ -4846,6 +4847,71 @@ function canEditReportPolicy(report: OnyxEntry<Report>, reportPolicy: OnyxEntry<
     return false;
 }
 
+/**
+ * Checks if the user can edit multiple transactions
+ */
+function canEditMultipleTransactions(
+    selectedTransactions: Transaction[],
+    reportActions: OnyxCollection<ReportActions>,
+    reports: OnyxCollection<Report>,
+    policies: OnyxCollection<Policy>,
+    areReportsSelected = false,
+    searchSnapshotData?: SearchResults['data'],
+): boolean {
+    if (areReportsSelected) {
+        return false;
+    }
+
+    if (selectedTransactions.length < 2) {
+        return false;
+    }
+
+    for (const transaction of selectedTransactions) {
+        const isUnreportedExpense = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
+        if (isUnreportedExpense) {
+            continue;
+        }
+
+        // Do not allow editing split expenses in bulk
+        if (transaction.comment?.source === CONST.IOU.TYPE.SPLIT || transaction.comment?.splits) {
+            return false;
+        }
+
+        const reportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}` as const;
+        const actionsForReport = {...(searchSnapshotData?.[reportActionsKey] ?? {}), ...(reportActions?.[reportActionsKey] ?? {})};
+        const reportAction = getIOUActionForTransactionID(Object.values(actionsForReport), transaction.transactionID);
+        const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}` as const;
+        const report = reports?.[reportKey] ?? (searchSnapshotData?.[reportKey] as OnyxEntry<Report>);
+        const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
+
+        const isApproved = isReportApproved({report});
+
+        if (isApproved || isSettled(report)) {
+            return false;
+        }
+
+        const fieldsToCheck = [
+            CONST.EDIT_REQUEST_FIELD.AMOUNT,
+            CONST.EDIT_REQUEST_FIELD.MERCHANT,
+            CONST.EDIT_REQUEST_FIELD.CATEGORY,
+            CONST.EDIT_REQUEST_FIELD.TAG,
+            CONST.EDIT_REQUEST_FIELD.DESCRIPTION,
+            CONST.EDIT_REQUEST_FIELD.DATE,
+            CONST.EDIT_REQUEST_FIELD.BILLABLE,
+            CONST.EDIT_REQUEST_FIELD.REIMBURSABLE,
+            CONST.EDIT_REQUEST_FIELD.TAX_RATE,
+        ];
+
+        const isTransactionEditable = fieldsToCheck.some((field) => canEditFieldOfMoneyRequest({reportAction, fieldToEdit: field, transaction, report, policy}));
+
+        if (!isTransactionEditable) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
 /**
  * Checks if the current user can edit the provided property of an expense
  *
@@ -4880,6 +4946,7 @@ function canEditFieldOfMoneyRequest({
         CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE,
         CONST.EDIT_REQUEST_FIELD.REIMBURSABLE,
         CONST.EDIT_REQUEST_FIELD.REPORT,
+        CONST.EDIT_REQUEST_FIELD.BILLABLE,
     ];
 
     if (!isMoneyRequestAction(reportAction) || !canEditMoneyRequest(reportAction, isChatReportArchived, report, policy, transaction)) {
@@ -4894,6 +4961,10 @@ function canEditFieldOfMoneyRequest({
     const iouMessage = getOriginalMessage(reportAction);
     const moneyRequestReport = report ?? (iouMessage?.IOUReportID ? (getReport(iouMessage?.IOUReportID, allReports) ?? ({} as Report)) : ({} as Report));
 
+    if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.BILLABLE && isInvoiceReport(moneyRequestReport) && isReportApproved({report: moneyRequestReport})) {
+        return false;
+    }
+
     if (isSettled(String(moneyRequestReport.reportID)) || isReportIDApproved(String(moneyRequestReport.reportID))) {
         return false;
     }
@@ -13420,6 +13491,7 @@ export {
     canHoldUnholdReportAction,
     canEditReportPolicy,
     canEditFieldOfMoneyRequest,
+    canEditMultipleTransactions,
     canEditMoneyRequest,
     canEditReportAction,
     canEditReportDescription,
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 1ddfa1ec6ab9..5cb7a778b9de 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -4959,6 +4959,40 @@ function getTableMinWidth(columns: SearchColumnType[]) {
     return minWidth;
 }
 
+/**
+ * Determine policyID based on selected transactions:
+ * - If all selected transactions belong to the same policy, use that policy
+ * - Otherwise, fall back to the user's active workspace policy
+ */
+function getSearchBulkEditPolicyID(
+    selectedTransactionIDs: string[],
+    activePolicyID: string | undefined,
+    allTransactions: OnyxCollection<OnyxTypes.Transaction> | undefined,
+    allReports: OnyxCollection<OnyxTypes.Report> | undefined,
+): string | undefined {
+    if (selectedTransactionIDs.length === 0) {
+        return activePolicyID;
+    }
+
+    const policyIDs = selectedTransactionIDs.map((transactionID) => {
+        const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+        if (!transaction?.reportID) {
+            return undefined;
+        }
+        const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
+        return report?.policyID;
+    });
+
+    const firstPolicyID = policyIDs.at(0);
+    const allSamePolicy = policyIDs.every((policyID) => policyID === firstPolicyID);
+
+    if (allSamePolicy && firstPolicyID) {
+        return firstPolicyID;
+    }
+
+    return activePolicyID;
+}
+
 function filterValidHasValues(hasValues: HasFilterValues | undefined, type: SearchDataTypes | undefined, translate: LocalizedTranslate): HasFilterValues | undefined {
     if (!hasValues || !type) {
         return undefined;
@@ -5090,6 +5124,7 @@ function applySelectionToItem(
 }
 
 export {
+    getSearchBulkEditPolicyID,
     getSuggestedSearches,
     getListItem,
     getSections,
diff --git a/src/libs/TagsOptionsListUtils.ts b/src/libs/TagsOptionsListUtils.ts
index b1e93e683785..b829fdf2161e 100644
--- a/src/libs/TagsOptionsListUtils.ts
+++ b/src/libs/TagsOptionsListUtils.ts
@@ -3,9 +3,10 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleCon
 import CONST from '@src/CONST';
 import type {Policy, PolicyTag, PolicyTagLists, PolicyTags, Transaction} from '@src/types/onyx';
 import type {PendingAction} from '@src/types/onyx/OnyxCommon';
+import {insertTagIntoTransactionTagsString} from './IOUUtils';
 import {hasEnabledOptions} from './OptionsListUtils';
 import type {Option} from './OptionsListUtils';
-import {getCleanedTagName, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isMultiLevelTags as isMultiLevelTagsPolicyUtils} from './PolicyUtils';
+import {getCleanedTagName, getTagList, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isMultiLevelTags as isMultiLevelTagsPolicyUtils} from './PolicyUtils';
 import tokenizedSearch from './tokenizedSearch';
 import {getTagArrayFromName, getTagForDisplay} from './TransactionUtils';
 
@@ -29,6 +30,16 @@ type TagVisibility = {
     shouldShow: boolean;
 };
 
+type UpdatedTransactionTagParams = {
+    transactionTag: string;
+    selectedTagName: string;
+    currentTag: string;
+    tagListIndex: number;
+    policyTags: OnyxEntry<PolicyTagLists>;
+    hasDependentTags: boolean;
+    hasMultipleTagLists: boolean;
+};
+
 /**
  * Transforms the provided tags into option objects.
  *
@@ -225,6 +236,35 @@ function getTagVisibility({
     });
 }
 
+/**
+ * Determines whether a dependent tag list should be shown based on the selected parent tag
+ * and available enabled tags for the current level.
+ */
+function shouldShowDependentTagList(tagListIndex: number, transactionTag: string | undefined, tags: PolicyTags | undefined): boolean {
+    if (tagListIndex === 0) {
+        return true;
+    }
+
+    const tagParts = getTagArrayFromName(transactionTag ?? '');
+    const previousTagValue = tagParts.at(tagListIndex - 1);
+    if (!previousTagValue) {
+        return false;
+    }
+
+    const parentTag = tagParts.slice(0, tagListIndex).join(':');
+    const availableTags = Object.values(tags ?? {}).filter((policyTag) => {
+        const filterRegex = policyTag.rules?.parentTagsFilter;
+        if (!filterRegex) {
+            return true;
+        }
+
+        const regex = new RegExp(filterRegex);
+        return regex.test(parentTag ?? '');
+    });
+
+    return availableTags.some((tag) => tag.enabled);
+}
+
 /**
  * Checks if any tag from policy tag lists exists in the transaction tag string.
  *
@@ -250,6 +290,45 @@ function hasMatchingTag(policyTagLists: OnyxEntry<PolicyTagLists>, transactionTa
     });
 }
 
+function getUpdatedTransactionTag({transactionTag, selectedTagName, currentTag, tagListIndex, policyTags, hasDependentTags, hasMultipleTagLists}: UpdatedTransactionTagParams): string {
+    const isSelectedTag = selectedTagName === currentTag;
+
+    if (hasDependentTags) {
+        const tagParts = transactionTag ? getTagArrayFromName(transactionTag) : [];
+
+        if (isSelectedTag) {
+            // Deselect: clear this and all child tags.
+            tagParts.splice(tagListIndex);
+        } else {
+            // Select new tag: replace this index and clear child tags.
+            tagParts.splice(tagListIndex, tagParts.length - tagListIndex, selectedTagName);
+
+            const policyTagLists = getTagLists(policyTags);
+            // Auto-select subsequent tags if there is only one enabled tag
+            for (let i = tagListIndex + 1; i < policyTagLists.length; i++) {
+                const availableNextLevelTags = getTagList(policyTags, i);
+                const enabledTags = Object.values(availableNextLevelTags.tags).filter((tag) => tag.enabled);
+
+                if (enabledTags.length === 1) {
+                    // If there is only one enabled tag, we can auto-select it.
+                    const firstTag = enabledTags.at(0);
+                    if (firstTag) {
+                        tagParts.push(firstTag.name);
+                    }
+                } else {
+                    // If there are no enabled tags or more than one, stop auto-selecting.
+                    break;
+                }
+            }
+        }
+
+        return tagParts.join(':');
+    }
+
+    // Independent tags (fallback): use comma-separated list.
+    return insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTagName, tagListIndex, hasMultipleTagLists);
+}
+
 /**
  * Gets enabled tags filtered by parent tag at a specific index level.
  *
@@ -274,5 +353,5 @@ function getEnabledTags(tags: PolicyTags, tag: string, index: number) {
     });
 }
 
-export {getTagsOptions, getTagListSections, hasEnabledTags, sortTags, getTagVisibility, hasMatchingTag, getEnabledTags};
+export {getTagsOptions, getTagListSections, hasEnabledTags, sortTags, getTagVisibility, hasMatchingTag, getUpdatedTransactionTag, shouldShowDependentTagList, getEnabledTags};
 export type {SelectedTagOption, TagVisibility, TagOption};
diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts
index 7b22962ecd85..729025f48128 100644
--- a/src/libs/actions/IOU/index.ts
+++ b/src/libs/actions/IOU/index.ts
@@ -38,7 +38,7 @@ import type {
     UpdateMoneyRequestParams,
 } from '@libs/API/parameters';
 import {WRITE_COMMANDS} from '@libs/API/types';
-import {convertToDisplayString} from '@libs/CurrencyUtils';
+import {convertToBackendAmount, convertToDisplayString, getCurrencyDecimals} from '@libs/CurrencyUtils';
 import DateUtils from '@libs/DateUtils';
 import {registerDeferredWrite} from '@libs/deferredLayoutWrite';
 import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -89,6 +89,7 @@ import {
 import {
     getAllReportActions,
     getIOUActionForReportID,
+    getIOUActionForTransactionID,
     getLastVisibleAction,
     getLastVisibleMessage,
     getOriginalMessage,
@@ -133,6 +134,7 @@ import {
     buildOptimisticSubmittedReportAction,
     buildOptimisticUnapprovedReportAction,
     canBeAutoReimbursed,
+    canEditFieldOfMoneyRequest,
     canSubmitAndIsAwaitingForCurrentUser,
     canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
     findSelfDMReportID,
@@ -200,6 +202,7 @@ import {endSubmitFollowUpActionSpan, setPendingSubmitFollowUpAction} from '@libs
 import {
     allHavePendingRTERViolation,
     buildOptimisticTransaction,
+    calculateTaxAmount,
     getAmount,
     getCategoryTaxCodeAndAmount,
     getClearedPendingFields,
@@ -207,6 +210,7 @@ import {
     getDistanceInMeters,
     getMerchant,
     getRateID,
+    getTaxValue,
     getUpdatedTransaction,
     getWaypoints,
     hasAnyTransactionWithoutRTERViolation,
@@ -239,7 +243,8 @@ import {buildPolicyData, generatePolicyID} from '@userActions/Policy/Policy';
 import type {BuildPolicyDataKeys} from '@userActions/Policy/Policy';
 import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag';
 import type {GuidedSetupData} from '@userActions/Report';
-import {buildInviteToRoomOnyxData, completeOnboarding, notifyNewAction, optimisticReportLastData} from '@userActions/Report';
+import {buildInviteToRoomOnyxData, completeOnboarding, createTransactionThreadReport, notifyNewAction, optimisticReportLastData} from '@userActions/Report';
+import {resolveDetachReceiptConflicts} from '@userActions/RequestConflictUtils';
 import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeWaypointsForAPI, stringifyWaypointsForAPI} from '@userActions/Transaction';
 import {getRemoveDraftTransactionsByIDsData, removeDraftTransaction, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit';
 import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow';
@@ -266,7 +271,6 @@ import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
 import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionChanges, TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction';
 import type {FileObject} from '@src/types/utils/Attachment';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import {resolveDetachReceiptConflicts} from '../RequestConflictUtils';
 
 type IOURequestType = ValueOf<typeof CONST.IOU.REQUEST_TYPE>;
 
@@ -4040,6 +4044,56 @@ function calculateDiffAmount(
     return null;
 }
 
+function getUpdatedMoneyRequestReportData(
+    iouReport: OnyxTypes.OnyxInputOrEntry<OnyxTypes.Report>,
+    updatedTransaction: OnyxTypes.OnyxInputOrEntry<OnyxTypes.Transaction>,
+    transaction: OnyxEntry<OnyxTypes.Transaction>,
+    isTransactionOnHold: boolean,
+    policy: OnyxEntry<OnyxTypes.Policy>,
+    actorAccountID?: number,
+    transactionChanges?: TransactionChanges,
+) {
+    const calculatedDiffAmount = calculateDiffAmount(iouReport, updatedTransaction, transaction);
+    const isTotalIndeterminate = calculatedDiffAmount === null;
+    const diff = calculatedDiffAmount ?? 0;
+
+    let updatedMoneyRequestReport: OnyxTypes.OnyxInputOrEntry<OnyxTypes.Report>;
+    if (!iouReport) {
+        updatedMoneyRequestReport = null;
+    } else if ((isExpenseReport(iouReport) || isInvoiceReportReportUtils(iouReport)) && !Number.isNaN(iouReport.total) && iouReport.total !== undefined) {
+        // For expense report, the amount is negative, so we should subtract total from diff
+        updatedMoneyRequestReport = {
+            ...iouReport,
+            total: iouReport.total - diff,
+        };
+        if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') {
+            updatedMoneyRequestReport.nonReimbursableTotal -= diff;
+        }
+        if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') {
+            updatedMoneyRequestReport.nonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount;
+        }
+        if (!isTransactionOnHold) {
+            if (typeof updatedMoneyRequestReport.unheldTotal === 'number') {
+                updatedMoneyRequestReport.unheldTotal -= diff;
+            }
+            if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') {
+                updatedMoneyRequestReport.unheldNonReimbursableTotal -= diff;
+            }
+            if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') {
+                updatedMoneyRequestReport.unheldNonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount;
+            }
+        }
+        // Only recalculate reportName when reimbursable status changes and the report uses a formula title
+        if (transactionChanges && 'reimbursable' in transactionChanges) {
+            updatedMoneyRequestReport = maybeUpdateReportNameForFormulaTitle(updatedMoneyRequestReport, policy);
+        }
+    } else {
+        updatedMoneyRequestReport = updateIOUOwnerAndTotal(iouReport, actorAccountID ?? CONST.DEFAULT_NUMBER_ID, diff, getCurrency(transaction), false, true, isTransactionOnHold);
+    }
+
+    return {updatedMoneyRequestReport, isTotalIndeterminate};
+}
+
 type GetUpdateMoneyRequestParamsType = {
     transactionID: string | undefined;
     transactionThreadReport: OnyxEntry<OnyxTypes.Report>;
@@ -4255,53 +4309,16 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
     }
 
     // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct.
-    const calculatedDiffAmount = calculateDiffAmount(iouReport, updatedTransaction, transaction);
-    // If calculatedDiffAmount is null it means we cannot calculate the new iou report total from front-end due to currency differences.
-    const isTotalIndeterminate = calculatedDiffAmount === null;
-    const diff = calculatedDiffAmount ?? 0;
-
-    let updatedMoneyRequestReport: OnyxTypes.OnyxInputOrEntry<OnyxTypes.Report>;
-    if (!iouReport) {
-        updatedMoneyRequestReport = null;
-    } else if ((isExpenseReport(iouReport) || isInvoiceReportReportUtils(iouReport)) && !Number.isNaN(iouReport.total) && iouReport.total !== undefined) {
-        // For expense report, the amount is negative, so we should subtract total from diff
-        updatedMoneyRequestReport = {
-            ...iouReport,
-            total: iouReport.total - diff,
-        };
-        if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') {
-            updatedMoneyRequestReport.nonReimbursableTotal -= diff;
-        }
-        if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.nonReimbursableTotal === 'number') {
-            updatedMoneyRequestReport.nonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount;
-        }
-        if (!isTransactionOnHold) {
-            if (typeof updatedMoneyRequestReport.unheldTotal === 'number') {
-                updatedMoneyRequestReport.unheldTotal -= diff;
-            }
-            if (!transaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') {
-                updatedMoneyRequestReport.unheldNonReimbursableTotal -= diff;
-            }
-            if (updatedTransaction && transaction?.reimbursable !== updatedTransaction?.reimbursable && typeof updatedMoneyRequestReport.unheldNonReimbursableTotal === 'number') {
-                updatedMoneyRequestReport.unheldNonReimbursableTotal += updatedTransaction.reimbursable ? -updatedTransaction.amount : updatedTransaction.amount;
-            }
-        }
-
-        // Only recalculate reportName when reimbursable status changes and the report uses a formula title
-        if ('reimbursable' in transactionChanges) {
-            updatedMoneyRequestReport = maybeUpdateReportNameForFormulaTitle(updatedMoneyRequestReport, policy);
-        }
-    } else {
-        updatedMoneyRequestReport = updateIOUOwnerAndTotal(
-            iouReport,
-            updatedReportAction?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID,
-            diff,
-            getCurrency(transaction),
-            false,
-            true,
-            isTransactionOnHold,
-        );
-    }
+    // If the diff is indeterminate we cannot calculate the new iou report total from front-end due to currency differences.
+    const {updatedMoneyRequestReport, isTotalIndeterminate} = getUpdatedMoneyRequestReportData(
+        iouReport,
+        updatedTransaction,
+        transaction,
+        isTransactionOnHold,
+        policy,
+        updatedReportAction?.actorAccountID,
+        transactionChanges,
+    );
 
     optimisticData.push(
         {
@@ -13194,6 +13211,522 @@ function addReportApprover(
     API.write(WRITE_COMMANDS.ADD_REPORT_APPROVER, params, onyxData);
 }
 
+function removeUnchangedBulkEditFields(
+    transactionChanges: TransactionChanges,
+    transaction: OnyxTypes.Transaction,
+    baseIOUReport: OnyxEntry<OnyxTypes.Report> | null,
+    policy: OnyxEntry<OnyxTypes.Policy>,
+): TransactionChanges {
+    const iouType = isInvoiceReportReportUtils(baseIOUReport ?? undefined) ? CONST.IOU.TYPE.INVOICE : CONST.IOU.TYPE.SUBMIT;
+    const allowNegative = shouldEnableNegative(baseIOUReport ?? undefined, policy, iouType);
+    const currentDetails = getTransactionDetails(transaction, undefined, policy, allowNegative);
+    if (!currentDetails) {
+        return transactionChanges;
+    }
+
+    const changeKeys = Object.keys(transactionChanges) as Array<keyof TransactionChanges>;
+    if (changeKeys.length === 0) {
+        return transactionChanges;
+    }
+
+    let filteredChanges: TransactionChanges = {};
+
+    for (const field of changeKeys) {
+        const nextValue = transactionChanges[field];
+        const currentValue = currentDetails[field as keyof TransactionDetails];
+
+        if (nextValue !== currentValue) {
+            filteredChanges = {
+                ...filteredChanges,
+                [field]: nextValue,
+            };
+        }
+    }
+
+    return filteredChanges;
+}
+
+type UpdateMultipleMoneyRequestsParams = {
+    transactionIDs: string[];
+    changes: TransactionChanges;
+    policy: OnyxEntry<OnyxTypes.Policy>;
+    reports: OnyxCollection<OnyxTypes.Report>;
+    transactions: OnyxCollection<OnyxTypes.Transaction>;
+    reportActions: OnyxCollection<OnyxTypes.ReportActions>;
+    policyCategories: OnyxCollection<OnyxTypes.PolicyCategories>;
+    policyTags: OnyxCollection<OnyxTypes.PolicyTagLists>;
+    hash?: number;
+    allPolicies?: OnyxCollection<OnyxTypes.Policy>;
+};
+
+function updateMultipleMoneyRequests({
+    transactionIDs,
+    changes,
+    policy,
+    reports,
+    transactions,
+    reportActions,
+    policyCategories,
+    policyTags,
+    hash,
+    allPolicies,
+}: UpdateMultipleMoneyRequestsParams) {
+    // Track running totals per report so multiple edits in the same report compound correctly.
+    const optimisticReportsByID: Record<string, OnyxTypes.Report> = {};
+    for (const transactionID of transactionIDs) {
+        const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+        if (!transaction) {
+            continue;
+        }
+
+        const iouReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`] ?? undefined;
+        const baseIouReport = iouReport?.reportID ? (optimisticReportsByID[iouReport.reportID] ?? iouReport) : iouReport;
+        const isFromExpenseReport = isExpenseReport(baseIouReport);
+
+        const transactionReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] ?? {};
+        let reportAction = getIOUActionForTransactionID(Object.values(transactionReportActions), transactionID);
+
+        // Track expenses created via self DM are stored with reportID = UNREPORTED_REPORT_ID ('0')
+        // because they have never been submitted to a report. As a result, the lookup above returns
+        // nothing — the IOU action is stored under the self DM report (chat with yourself, unique
+        // per user) rather than under transaction.reportID. Without this fallback we cannot resolve
+        // the transaction thread, which means no optimistic MODIFIED_EXPENSE comment would be
+        // generated during bulk edit for these expenses.
+        if (!reportAction && (!transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID)) {
+            const selfDMReportID = findSelfDMReportID(reports);
+            if (selfDMReportID) {
+                const selfDMReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${selfDMReportID}`] ?? {};
+                reportAction = getIOUActionForTransactionID(Object.values(selfDMReportActions), transactionID);
+            }
+        }
+
+        let transactionThreadReportID = transaction.transactionThreadReportID ?? reportAction?.childReportID;
+        let transactionThread = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
+
+        // Offline-created expenses can be missing a transaction thread until it's opened once.
+        // Ensure the thread exists before adding optimistic MODIFIED_EXPENSE actions so
+        // bulk-edit comments are visible immediately while still offline.
+        let didCreateThreadInThisIteration = false;
+        if (!transactionThreadReportID && iouReport?.reportID) {
+            const optimisticTransactionThread = createTransactionThreadReport(undefined, currentUserEmail, userAccountID, iouReport, reportAction, transaction);
+            if (optimisticTransactionThread?.reportID) {
+                transactionThreadReportID = optimisticTransactionThread.reportID;
+                transactionThread = optimisticTransactionThread;
+                didCreateThreadInThisIteration = true;
+            }
+        }
+
+        const isUnreportedExpense = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
+        // Category, tag, tax, and billable only apply to expense/invoice reports and unreported (track) expenses.
+        // For plain IOU transactions these fields are not applicable and must be silently skipped.
+        const supportsExpenseFields = isUnreportedExpense || isFromExpenseReport || isInvoiceReportReportUtils(baseIouReport ?? undefined);
+        // Use the transaction's own policy for all per-transaction checks (permissions, tax, change-diffing).
+        // Falls back to the shared bulk-edit policy when the transaction's workspace cannot be resolved.
+        const transactionPolicy = (iouReport?.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${iouReport.policyID}`] : undefined) ?? policy;
+        const canEditField = (field: ValueOf<typeof CONST.EDIT_REQUEST_FIELD>) => {
+            // Unreported (track) expenses have no report, so there is no reportAction to validate against.
+            // They are never approved or settled, so all bulk-editable fields are allowed.
+            if (isUnreportedExpense) {
+                return true;
+            }
+
+            return canEditFieldOfMoneyRequest({reportAction, fieldToEdit: field, transaction, report: iouReport, policy: transactionPolicy});
+        };
+
+        let transactionChanges: TransactionChanges = {};
+
+        if (changes.merchant && canEditField(CONST.EDIT_REQUEST_FIELD.MERCHANT)) {
+            transactionChanges.merchant = changes.merchant;
+        }
+        if (changes.created && canEditField(CONST.EDIT_REQUEST_FIELD.DATE)) {
+            transactionChanges.created = changes.created;
+        }
+        if (changes.amount !== undefined && canEditField(CONST.EDIT_REQUEST_FIELD.AMOUNT)) {
+            transactionChanges.amount = changes.amount;
+        }
+        if (changes.currency && canEditField(CONST.EDIT_REQUEST_FIELD.CURRENCY)) {
+            transactionChanges.currency = changes.currency;
+        }
+        if (changes.category !== undefined && supportsExpenseFields && canEditField(CONST.EDIT_REQUEST_FIELD.CATEGORY)) {
+            transactionChanges.category = changes.category;
+        }
+        if (changes.tag && supportsExpenseFields && canEditField(CONST.EDIT_REQUEST_FIELD.TAG)) {
+            transactionChanges.tag = changes.tag;
+        }
+        if (changes.comment && canEditField(CONST.EDIT_REQUEST_FIELD.DESCRIPTION)) {
+            transactionChanges.comment = getParsedComment(changes.comment);
+        }
+        if (changes.taxCode && supportsExpenseFields && canEditField(CONST.EDIT_REQUEST_FIELD.TAX_RATE)) {
+            transactionChanges.taxCode = changes.taxCode;
+            const taxValue = getTaxValue(transactionPolicy, transaction, changes.taxCode);
+            const decimals = getCurrencyDecimals(getCurrency(transaction));
+            const effectiveAmount = transactionChanges.amount !== undefined ? Math.abs(transactionChanges.amount) : Math.abs(getAmount(transaction));
+            const taxAmount = calculateTaxAmount(taxValue, effectiveAmount, decimals);
+            transactionChanges.taxAmount = convertToBackendAmount(taxAmount);
+        }
+
+        if (changes.billable !== undefined && supportsExpenseFields && canEditField(CONST.EDIT_REQUEST_FIELD.BILLABLE)) {
+            transactionChanges.billable = changes.billable;
+        }
+        if (changes.reimbursable !== undefined && canEditField(CONST.EDIT_REQUEST_FIELD.REIMBURSABLE)) {
+            transactionChanges.reimbursable = changes.reimbursable;
+        }
+
+        transactionChanges = removeUnchangedBulkEditFields(transactionChanges, transaction, baseIouReport, transactionPolicy);
+
+        const updates: Record<string, string | number | boolean> = {};
+        if (transactionChanges.merchant) {
+            updates.merchant = transactionChanges.merchant;
+        }
+        if (transactionChanges.created) {
+            updates.created = transactionChanges.created;
+        }
+        if (transactionChanges.currency) {
+            updates.currency = transactionChanges.currency;
+        }
+        if (transactionChanges.category !== undefined) {
+            updates.category = transactionChanges.category;
+        }
+        if (transactionChanges.tag) {
+            updates.tag = transactionChanges.tag;
+        }
+        if (transactionChanges.comment) {
+            updates.comment = transactionChanges.comment;
+        }
+        if (transactionChanges.taxCode) {
+            updates.taxCode = transactionChanges.taxCode;
+        }
+        if (transactionChanges.taxAmount !== undefined) {
+            updates.taxAmount = transactionChanges.taxAmount;
+        }
+        if (transactionChanges.amount !== undefined) {
+            updates.amount = transactionChanges.amount;
+        }
+        if (transactionChanges.billable !== undefined) {
+            updates.billable = transactionChanges.billable;
+        }
+        if (transactionChanges.reimbursable !== undefined) {
+            updates.reimbursable = transactionChanges.reimbursable;
+        }
+
+        // Skip if no updates
+        if (Object.keys(updates).length === 0) {
+            continue;
+        }
+
+        // Generate optimistic report action ID
+        const modifiedExpenseReportActionID = NumberUtils.rand64();
+
+        const optimisticData: Array<
+            OnyxUpdate<
+                typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
+            >
+        > = [];
+        const successData: Array<
+            OnyxUpdate<
+                typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
+            >
+        > = [];
+        const failureData: Array<
+            OnyxUpdate<
+                typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
+            >
+        > = [];
+        const snapshotOptimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.SNAPSHOT>> = [];
+        const snapshotFailureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.SNAPSHOT>> = [];
+
+        // Pending fields for the transaction
+        const pendingFields: OnyxTypes.Transaction['pendingFields'] = Object.fromEntries(Object.keys(transactionChanges).map((field) => [field, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE]));
+        const clearedPendingFields = getClearedPendingFields(transactionChanges);
+
+        const errorFields = Object.fromEntries(Object.keys(pendingFields).map((field) => [field, getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage')]));
+
+        // Build updated transaction
+        const updatedTransaction = getUpdatedTransaction({
+            transaction,
+            transactionChanges,
+            isFromExpenseReport,
+            policy: transactionPolicy,
+        });
+        const isTransactionOnHold = isOnHold(transaction);
+
+        // Optimistically update violations so they disappear immediately when the edited field resolves them.
+        // Skip for unreported expenses: they have no iouReport context so isSelfDM() returns false,
+        // which would incorrectly trigger policy-required violations (e.g. missingCategory).
+        let optimisticViolationsData: ReturnType<typeof ViolationsUtils.getViolationsOnyxData> | undefined;
+        let currentTransactionViolations: OnyxTypes.TransactionViolation[] | undefined;
+        if (transactionPolicy && !isUnreportedExpense) {
+            currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? [];
+            let optimisticViolations =
+                transactionChanges.amount !== undefined || transactionChanges.created || transactionChanges.currency
+                    ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION)
+                    : currentTransactionViolations;
+            optimisticViolations =
+                transactionChanges.category !== undefined && transactionChanges.category === ''
+                    ? optimisticViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY)
+                    : optimisticViolations;
+            const transactionPolicyTagList = policyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${transactionPolicy?.id}`] ?? {};
+            const transactionPolicyCategories = policyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${transactionPolicy?.id}`] ?? {};
+            optimisticViolationsData = ViolationsUtils.getViolationsOnyxData(
+                updatedTransaction,
+                optimisticViolations,
+                transactionPolicy,
+                transactionPolicyTagList,
+                transactionPolicyCategories,
+                hasDependentTags(transactionPolicy, transactionPolicyTagList),
+                isInvoiceReportReportUtils(iouReport),
+                isSelfDM(iouReport),
+                iouReport,
+                isFromExpenseReport,
+            );
+            optimisticData.push(optimisticViolationsData);
+            failureData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
+                value: currentTransactionViolations,
+            });
+        }
+
+        // Optimistic transaction update
+        optimisticData.push({
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+            value: {
+                ...updatedTransaction,
+                pendingFields,
+                isLoading: false,
+                errorFields: null,
+            },
+        });
+
+        // Optimistically update the search snapshot so the search list reflects the
+        // new values immediately (the snapshot is the exclusive data source for search
+        // result rendering and is not automatically updated by the TRANSACTION write above).
+        if (hash) {
+            snapshotOptimisticData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}` as const,
+                value: {
+                    // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
+                    data: {
+                        [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: updatedTransaction,
+                        ...(optimisticViolationsData && {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]: optimisticViolationsData.value}),
+                    },
+                },
+            });
+            snapshotFailureData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}` as const,
+                value: {
+                    // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
+                    data: {
+                        [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+                        ...(currentTransactionViolations && {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]: currentTransactionViolations}),
+                    },
+                },
+            });
+        }
+
+        // To build proper offline update message, we need to include the currency
+        const optimisticTransactionChanges =
+            transactionChanges?.amount !== undefined && !transactionChanges?.currency ? {...transactionChanges, currency: getCurrency(transaction)} : transactionChanges;
+
+        // Build optimistic modified expense report action
+        const optimisticReportAction = buildOptimisticModifiedExpenseReportAction(
+            transactionThread,
+            transaction,
+            optimisticTransactionChanges,
+            isFromExpenseReport,
+            transactionPolicy,
+            updatedTransaction,
+        );
+
+        const {updatedMoneyRequestReport, isTotalIndeterminate} = getUpdatedMoneyRequestReportData(
+            baseIouReport,
+            updatedTransaction,
+            transaction,
+            isTransactionOnHold,
+            transactionPolicy,
+            optimisticReportAction?.actorAccountID,
+            transactionChanges,
+        );
+
+        if (updatedMoneyRequestReport) {
+            if (updatedMoneyRequestReport.reportID) {
+                optimisticReportsByID[updatedMoneyRequestReport.reportID] = updatedMoneyRequestReport;
+            }
+            optimisticData.push(
+                {
+                    onyxMethod: Onyx.METHOD.MERGE,
+                    key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`,
+                    value: {...updatedMoneyRequestReport, ...(isTotalIndeterminate && {pendingFields: {total: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}})},
+                },
+                {
+                    onyxMethod: Onyx.METHOD.MERGE,
+                    key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.parentReportID}`,
+                    value: getOutstandingChildRequest(updatedMoneyRequestReport),
+                },
+            );
+            successData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`,
+                value: {pendingAction: null, ...(isTotalIndeterminate && {pendingFields: {total: null}})},
+            });
+            failureData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`,
+                value: {...iouReport, ...(isTotalIndeterminate && {pendingFields: {total: null}})},
+            });
+        }
+
+        // Optimistic report action
+        let backfilledCreatedActionID: string | undefined;
+        if (transactionThreadReportID) {
+            // Backfill a CREATED action for threads never opened locally so
+            // MoneyRequestView renders and the skeleton doesn't loop offline.
+            // Skip when the thread was just created above (openReport handles it).
+            const threadReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {};
+            const hasCreatedAction = didCreateThreadInThisIteration || Object.values(threadReportActions).some((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED);
+            const optimisticCreatedValue: Record<string, Partial<OnyxTypes.ReportAction>> = {};
+            if (!hasCreatedAction) {
+                const optimisticCreatedAction = buildOptimisticCreatedReportAction(CONST.REPORT.OWNER_EMAIL_FAKE);
+                optimisticCreatedAction.pendingAction = null;
+                backfilledCreatedActionID = optimisticCreatedAction.reportActionID;
+                optimisticCreatedValue[optimisticCreatedAction.reportActionID] = optimisticCreatedAction;
+            }
+
+            optimisticData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+                value: {
+                    ...optimisticCreatedValue,
+                    [modifiedExpenseReportActionID]: {
+                        ...optimisticReportAction,
+                        reportActionID: modifiedExpenseReportActionID,
+                    },
+                },
+            });
+            optimisticData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`,
+                value: {
+                    lastReadTime: optimisticReportAction.created,
+                    reportID: transactionThreadReportID,
+                },
+            });
+            failureData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`,
+                value: {
+                    lastReadTime: transactionThread?.lastReadTime,
+                },
+            });
+        }
+
+        // Success data - clear pending fields
+        successData.push({
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+            value: {
+                pendingFields: clearedPendingFields,
+            },
+        });
+
+        if (transactionThreadReportID) {
+            successData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+                value: {
+                    [modifiedExpenseReportActionID]: {pendingAction: null},
+                    // Remove the backfilled CREATED action so it doesn't duplicate one from OpenReport
+                    ...(backfilledCreatedActionID ? {[backfilledCreatedActionID]: null} : {}),
+                },
+            });
+        }
+
+        // Failure data - revert transaction
+        failureData.push({
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+            value: {
+                ...transaction,
+                pendingFields: clearedPendingFields,
+                errorFields,
+            },
+        });
+
+        // Failure data - remove optimistic report action
+        if (transactionThreadReportID) {
+            failureData.push({
+                onyxMethod: Onyx.METHOD.MERGE,
+                key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+                value: {
+                    [modifiedExpenseReportActionID]: {
+                        pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+                        errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage'),
+                    },
+                    ...(backfilledCreatedActionID ? {[backfilledCreatedActionID]: null} : {}),
+                },
+            });
+        }
+
+        const params = {
+            transactionID,
+            reportActionID: modifiedExpenseReportActionID,
+            updates: JSON.stringify(updates),
+        };
+
+        API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST, params, {
+            optimisticData: [...optimisticData, ...snapshotOptimisticData] as Array<
+                OnyxUpdate<
+                    | typeof ONYXKEYS.COLLECTION.TRANSACTION
+                    | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS
+                    | typeof ONYXKEYS.COLLECTION.SNAPSHOT
+                    | typeof ONYXKEYS.COLLECTION.REPORT
+                    | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
+                >
+            >,
+            successData,
+            failureData: [...failureData, ...snapshotFailureData] as Array<
+                OnyxUpdate<
+                    | typeof ONYXKEYS.COLLECTION.TRANSACTION
+                    | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS
+                    | typeof ONYXKEYS.COLLECTION.SNAPSHOT
+                    | typeof ONYXKEYS.COLLECTION.REPORT
+                    | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
+                >
+            >,
+        });
+    }
+}
+
+/**
+ * Initializes the bulk-edit draft transaction under one fixed placeholder ID.
+ * We keep a single draft in Onyx to store the shared edits for a multi-select,
+ * then apply those edits to each real transaction later. The placeholder ID is
+ * just the storage key and never equals any actual transactionID.
+ */
+function initBulkEditDraftTransaction(selectedTransactionIDs: string[]) {
+    Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`, {
+        transactionID: CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID,
+        selectedTransactionIDs,
+    });
+}
+
+/**
+ * Clears the draft transaction used for bulk editing
+ */
+function clearBulkEditDraftTransaction() {
+    Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`, null);
+}
+
+/**
+ * Updates the draft transaction for bulk editing multiple expenses
+ */
+function updateBulkEditDraftTransaction(transactionChanges: NullishDeep<OnyxTypes.Transaction>) {
+    Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`, transactionChanges);
+}
+
 function rejectExpenseReport(
     report: OnyxTypes.Report,
     targetAccountID: number,
@@ -13478,6 +14011,10 @@ export {
     getUpdateMoneyRequestParams,
     getUpdateTrackExpenseParams,
     getReportPreviewAction,
+    updateMultipleMoneyRequests,
+    initBulkEditDraftTransaction,
+    clearBulkEditDraftTransaction,
+    updateBulkEditDraftTransaction,
     mergePolicyRecentlyUsedCurrencies,
     mergePolicyRecentlyUsedCategories,
     getAllPersonalDetails,
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx
new file mode 100644
index 000000000000..99d0ff3c5ef7
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx
@@ -0,0 +1,171 @@
+import {useFocusEffect} from '@react-navigation/native';
+import React, {useMemo, useRef, useState} from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import {useSearchStateContext} from '@components/Search/SearchContext';
+import isTextInputFocused from '@components/TextInput/BaseTextInput/isTextInputFocused';
+import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import {convertToBackendAmount} from '@libs/CurrencyUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import {isInvoiceReport, isIOUReport, shouldEnableNegative} from '@libs/ReportUtils';
+import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils';
+import MoneyRequestAmountForm from '@pages/iou/MoneyRequestAmountForm';
+import IOURequestStepCurrencyModal from '@pages/iou/request/step/IOURequestStepCurrencyModal';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {withSnapshotReports, withSnapshotTransactions} from './SearchEditMultipleUtils';
+
+type CurrentMoney = {amount: string; currency: string};
+
+function SearchEditMultipleAmountPage() {
+    const {translate} = useLocalize();
+    const {currentSearchResults} = useSearchStateContext();
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+    const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+    const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+    const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
+    const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+
+    const snapshotData = currentSearchResults?.data;
+    const mergedTransactions = withSnapshotTransactions(allTransactions, snapshotData);
+    const mergedReports = withSnapshotReports(allReports, snapshotData);
+
+    const textInput = useRef<BaseTextInputRef | null>(null);
+    const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
+
+    const selectedTransactionIDs = useMemo(() => draftTransaction?.selectedTransactionIDs ?? [], [draftTransaction?.selectedTransactionIDs]);
+
+    const policyID = getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, mergedTransactions, mergedReports);
+    const policy = policyID ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : undefined;
+    const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD;
+
+    const initialCurrency = draftTransaction?.currency ?? policyCurrency;
+    const [selectedCurrency, setSelectedCurrency] = useState(initialCurrency);
+    const [isCurrencyPickerVisible, setIsCurrencyPickerVisible] = useState(false);
+
+    useFocusEffect(() => {
+        if (isCurrencyPickerVisible) {
+            return;
+        }
+        focusTimeoutRef.current = setTimeout(() => textInput.current?.focus(), CONST.ANIMATED_TRANSITION + 100);
+        return () => {
+            if (!focusTimeoutRef.current) {
+                return;
+            }
+            clearTimeout(focusTimeoutRef.current);
+        };
+    });
+
+    const amount = draftTransaction?.amount ?? 0;
+    const allowNegative = useMemo(() => {
+        if (!selectedTransactionIDs.length) {
+            return false;
+        }
+
+        return selectedTransactionIDs.every((transactionID) => {
+            const transaction = mergedTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+            if (!transaction) {
+                return false;
+            }
+
+            // Self-DM track expenses have no IOU report — they always support negative amounts
+            if (!transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
+                return true;
+            }
+
+            const transactionReport = mergedReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
+            const iouReport =
+                transactionReport?.type === CONST.REPORT.TYPE.CHAT && transactionReport?.parentReportID
+                    ? mergedReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionReport.parentReportID}`]
+                    : transactionReport;
+            if (!iouReport) {
+                return false;
+            }
+
+            const transactionPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${iouReport.policyID}`];
+            const iouType = isInvoiceReport(iouReport) ? CONST.IOU.TYPE.INVOICE : CONST.IOU.TYPE.SUBMIT;
+            return shouldEnableNegative(iouReport, transactionPolicy, iouType);
+        });
+    }, [selectedTransactionIDs, mergedTransactions, mergedReports, policies]);
+    const isP2P = selectedTransactionIDs.some((transactionID) => {
+        const transaction = mergedTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+        if (!transaction) {
+            return false;
+        }
+        const report = mergedReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
+        return isIOUReport(report);
+    });
+
+    const isAnyInvoice = selectedTransactionIDs.some((transactionID) => {
+        const transaction = mergedTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+        if (!transaction) {
+            return false;
+        }
+        const transactionReport = mergedReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
+        const iouReport =
+            transactionReport?.type === CONST.REPORT.TYPE.CHAT && transactionReport?.parentReportID
+                ? mergedReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionReport.parentReportID}`]
+                : transactionReport;
+        return isInvoiceReport(iouReport);
+    });
+
+    const amountForForm = allowNegative ? amount : Math.abs(amount);
+
+    const saveAmount = (currentMoney: CurrentMoney) => {
+        const newAmount = convertToBackendAmount(Number.parseFloat(currentMoney.amount));
+        // TODO: Currency update should be handled in a separate PR
+        updateBulkEditDraftTransaction({
+            amount: newAmount,
+        });
+        Navigation.goBack();
+    };
+
+    const showCurrencyPicker = () => {
+        if (isTextInputFocused(textInput)) {
+            textInput.current?.blur();
+        }
+        setIsCurrencyPickerVisible(true);
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID="SearchEditMultipleAmountPage"
+        >
+            <HeaderWithBackButton
+                title={translate('iou.amount')}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <IOURequestStepCurrencyModal
+                isPickerVisible={isCurrencyPickerVisible}
+                hidePickerModal={() => setIsCurrencyPickerVisible(false)}
+                headerText={translate('common.selectCurrency')}
+                value={selectedCurrency}
+                onInputChange={(value) => setSelectedCurrency(value)}
+            />
+            <MoneyRequestAmountForm
+                amount={amountForForm}
+                currency={selectedCurrency}
+                isEditing
+                isP2P={isP2P}
+                iouType={isAnyInvoice ? CONST.IOU.TYPE.INVOICE : CONST.IOU.TYPE.SUBMIT}
+                // TODO: Enable currency picker in a separate PR
+                isCurrencyPressable={false}
+                // TODO: Enable currency symbol in a separate PR
+                hideCurrencySymbol
+                ref={(e: BaseTextInputRef | null) => {
+                    textInput.current = e;
+                }}
+                onCurrencyButtonPress={showCurrencyPicker}
+                onSubmitButtonPress={saveAmount}
+                allowFlippingAmount={allowNegative}
+            />
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleAmountPage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx
new file mode 100644
index 000000000000..f467fdf001c5
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx
@@ -0,0 +1,83 @@
+import {useRoute} from '@react-navigation/native';
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
+import type {ListItem} from '@components/SelectionList/ListItem/types';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import SCREENS from '@src/SCREENS';
+
+type BooleanOption = ListItem & {
+    value: boolean;
+};
+
+function SearchEditMultipleBooleanPage() {
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+    const route = useRoute();
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+
+    const isBillableScreen = route.name === SCREENS.SEARCH.EDIT_MULTIPLE_BILLABLE_RHP;
+    const selectedValue = isBillableScreen ? draftTransaction?.billable : draftTransaction?.reimbursable;
+    const title = isBillableScreen ? translate('common.billable') : translate('common.reimbursable');
+    const testID = isBillableScreen ? 'SearchEditMultipleBillablePage' : 'SearchEditMultipleReimbursablePage';
+
+    const items = useMemo(
+        () => [
+            {
+                value: true,
+                keyForList: CONST.SEARCH.BOOLEAN.YES,
+                text: translate('common.yes'),
+                isSelected: selectedValue === true,
+            },
+            {
+                value: false,
+                keyForList: CONST.SEARCH.BOOLEAN.NO,
+                text: translate('common.no'),
+                isSelected: selectedValue === false,
+            },
+        ],
+        [selectedValue, translate],
+    );
+
+    const selectValue = (item: BooleanOption) => {
+        const shouldClear = selectedValue === item.value;
+        if (isBillableScreen) {
+            updateBulkEditDraftTransaction({billable: shouldClear ? null : item.value});
+        } else {
+            updateBulkEditDraftTransaction({reimbursable: shouldClear ? null : item.value});
+        }
+        Navigation.goBack();
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID={testID}
+        >
+            <HeaderWithBackButton
+                title={title}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <View style={[styles.flex1]}>
+                <SelectionList
+                    shouldSingleExecuteRowSelect
+                    data={items}
+                    ListItem={SingleSelectListItem}
+                    onSelectRow={selectValue}
+                />
+            </View>
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleBooleanPage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx
new file mode 100644
index 000000000000..c5c5ae732d1d
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import CategoryPicker from '@components/CategoryPicker';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import type {ListItem} from '@components/SelectionList/types';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useSearchBulkEditPolicyID from '@hooks/useSearchBulkEditPolicyID';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+function SearchEditMultipleCategoryPage() {
+    const {translate} = useLocalize();
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+
+    const policyID = useSearchBulkEditPolicyID();
+
+    const currentCategory = draftTransaction?.category ?? '';
+
+    const saveCategory = (item: ListItem) => {
+        const nextCategory = item.searchText ?? '';
+        if (!nextCategory || nextCategory === currentCategory) {
+            updateBulkEditDraftTransaction({
+                category: null,
+            });
+            Navigation.goBack();
+            return;
+        }
+        updateBulkEditDraftTransaction({
+            category: nextCategory,
+        });
+        Navigation.goBack();
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID="SearchEditMultipleCategoryPage"
+        >
+            <HeaderWithBackButton
+                title={translate('common.category')}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <CategoryPicker
+                policyID={policyID}
+                selectedCategory={currentCategory}
+                onSubmit={saveCategory}
+            />
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleCategoryPage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx
new file mode 100644
index 000000000000..d0188800f5e5
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import {View} from 'react-native';
+import DatePicker from '@components/DatePicker';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import Navigation from '@libs/Navigation/Navigation';
+import {isValidDate} from '@libs/ValidationUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/SearchEditMultipleDateForm';
+
+function SearchEditMultipleDatePage() {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+
+    const currentDate = draftTransaction?.created ?? '';
+
+    const validate = (value: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM> => {
+        const errors: FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM> = {};
+        const dateValue = value.date;
+        if (dateValue && !isValidDate(dateValue)) {
+            errors.date = translate('common.error.dateInvalid');
+        }
+        return errors;
+    };
+
+    const saveDate = (value: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM>) => {
+        const newDate = value.date;
+        updateBulkEditDraftTransaction({
+            created: newDate,
+        });
+        Navigation.goBack();
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID="SearchEditMultipleDatePage"
+        >
+            <HeaderWithBackButton
+                title={translate('common.date')}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <FormProvider
+                style={[styles.flexGrow1, styles.ph5]}
+                formID={ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM}
+                onSubmit={saveDate}
+                validate={validate}
+                submitButtonText={translate('common.save')}
+                enabledWhenOffline
+            >
+                <View style={styles.mb4}>
+                    <InputWrapper
+                        InputComponent={DatePicker}
+                        inputID={INPUT_IDS.DATE}
+                        label={translate('common.date')}
+                        autoFocus
+                        defaultValue={currentDate}
+                        maxDate={CONST.CALENDAR_PICKER.MAX_DATE}
+                        minDate={CONST.CALENDAR_PICKER.MIN_DATE}
+                    />
+                </View>
+            </FormProvider>
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleDatePage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx
new file mode 100644
index 000000000000..d23491a63c28
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import {addErrorMessage} from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/SearchEditMultipleDescriptionForm';
+
+function SearchEditMultipleDescriptionPage() {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const {inputCallbackRef} = useAutoFocusInput();
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+
+    const currentDescription = draftTransaction?.comment?.comment ?? '';
+
+    const validate = (value: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM> => {
+        const errors: FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM> = {};
+
+        if ((value.description?.length ?? 0) > CONST.DESCRIPTION_LIMIT) {
+            addErrorMessage(errors, INPUT_IDS.DESCRIPTION, translate('common.error.characterLimitExceedCounter', value.description?.length ?? 0, CONST.DESCRIPTION_LIMIT));
+        }
+
+        return errors;
+    };
+
+    const saveDescription = (value: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM>) => {
+        const newDescription = value.description?.trim() ?? '';
+        updateBulkEditDraftTransaction({
+            comment: {comment: newDescription},
+        });
+        Navigation.goBack();
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID="SearchEditMultipleDescriptionPage"
+        >
+            <HeaderWithBackButton
+                title={translate('common.description')}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <FormProvider
+                style={[styles.flexGrow1, styles.ph5]}
+                formID={ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM}
+                onSubmit={saveDescription}
+                validate={validate}
+                submitButtonText={translate('common.save')}
+                enabledWhenOffline
+            >
+                <View style={styles.mb4}>
+                    <InputWrapper
+                        valueType="string"
+                        InputComponent={TextInput}
+                        inputID={INPUT_IDS.DESCRIPTION}
+                        name={INPUT_IDS.DESCRIPTION}
+                        defaultValue={currentDescription}
+                        label={translate('common.description')}
+                        accessibilityLabel={translate('common.description')}
+                        role={CONST.ROLE.PRESENTATION}
+                        ref={inputCallbackRef}
+                        autoGrowHeight
+                        maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight}
+                        shouldSubmitForm
+                        type="markdown"
+                    />
+                </View>
+            </FormProvider>
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleDescriptionPage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx
new file mode 100644
index 000000000000..b60ce401bc53
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import Navigation from '@libs/Navigation/Navigation';
+import {isInvalidMerchantValue, isValidInputLength} from '@libs/ValidationUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/SearchEditMultipleMerchantForm';
+
+function SearchEditMultipleMerchantPage() {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const {inputCallbackRef} = useAutoFocusInput();
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+
+    const currentMerchant = draftTransaction?.merchant ?? '';
+
+    const validate = (value: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM> => {
+        const errors: FormInputErrors<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM> = {};
+        const {isValid, byteLength} = isValidInputLength(value.merchant ?? '', CONST.MERCHANT_NAME_MAX_BYTES);
+
+        const trimmedMerchant = (value.merchant ?? '').trim();
+        if (trimmedMerchant && isInvalidMerchantValue(trimmedMerchant)) {
+            errors.merchant = translate('iou.error.invalidMerchant');
+        } else if (!isValid) {
+            errors.merchant = translate('common.error.characterLimitExceedCounter', byteLength, CONST.MERCHANT_NAME_MAX_BYTES);
+        }
+
+        return errors;
+    };
+
+    const saveMerchant = (value: FormOnyxValues<typeof ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM>) => {
+        const newMerchant = value.merchant?.trim() ?? '';
+        updateBulkEditDraftTransaction({
+            merchant: newMerchant,
+        });
+        Navigation.goBack();
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID="SearchEditMultipleMerchantPage"
+        >
+            <HeaderWithBackButton
+                title={translate('common.merchant')}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <FormProvider
+                style={[styles.flexGrow1, styles.ph5]}
+                formID={ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM}
+                onSubmit={saveMerchant}
+                validate={validate}
+                submitButtonText={translate('common.save')}
+                enabledWhenOffline
+            >
+                <View style={styles.mb4}>
+                    <InputWrapper
+                        InputComponent={TextInput}
+                        inputID={INPUT_IDS.MERCHANT}
+                        name={INPUT_IDS.MERCHANT}
+                        defaultValue={currentMerchant}
+                        label={translate('common.merchant')}
+                        accessibilityLabel={translate('common.merchant')}
+                        role={CONST.ROLE.PRESENTATION}
+                        ref={inputCallbackRef}
+                    />
+                </View>
+            </FormProvider>
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleMerchantPage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx
new file mode 100644
index 000000000000..7b977e78bc30
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx
@@ -0,0 +1,317 @@
+import React, {useEffect} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import Button from '@components/Button';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {clearBulkEditDraftTransaction, updateMultipleMoneyRequests} from '@libs/actions/IOU';
+import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import {hasEnabledOptions} from '@libs/OptionsListUtils';
+import {getCleanedTagName, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils} from '@libs/PolicyUtils';
+import {canEditFieldOfMoneyRequest, isInvoiceReport, isIOUReport} from '@libs/ReportUtils';
+import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils';
+import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils';
+import {getTagArrayFromName, getTaxName, isDistanceRequest, isManagedCardTransaction, isPerDiemRequest, isTimeRequest} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
+import type {TransactionChanges} from '@src/types/onyx/Transaction';
+import {getTransactionEditContext, withSnapshotReportActions, withSnapshotReports, withSnapshotTransactions} from './SearchEditMultipleUtils';
+
+function SearchEditMultiplePage() {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const {currentSearchHash, currentSearchResults} = useSearchStateContext();
+    const {clearSelectedTransactions} = useSearchActionsContext();
+    const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+    const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+    const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
+    const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+    const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
+    const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS);
+    const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES);
+
+    const snapshotData = currentSearchResults?.data;
+    const mergedTransactions = withSnapshotTransactions(allTransactions, snapshotData);
+    const mergedReportActions = withSnapshotReportActions(allReportActions, snapshotData);
+    const mergedReports = withSnapshotReports(allReports, snapshotData);
+
+    const selectedTransactionIDs = draftTransaction?.selectedTransactionIDs ?? [];
+
+    const selectedTransactionContexts = selectedTransactionIDs.flatMap((transactionID) => {
+        const context = getTransactionEditContext(transactionID, mergedTransactions, mergedReports, mergedReportActions, policies);
+        return context ? [context] : [];
+    });
+
+    const hasCustomUnitTransaction = selectedTransactionContexts.some(({transaction}) => isDistanceRequest(transaction) || isPerDiemRequest(transaction));
+
+    const hasPerDiemOrTimeTransaction = selectedTransactionContexts.some(({transaction}) => isPerDiemRequest(transaction) || isTimeRequest(transaction));
+
+    const isFieldDisabledForAnyTransaction = (field: ValueOf<typeof CONST.EDIT_REQUEST_FIELD>) =>
+        selectedTransactionContexts.some(({transaction, report, reportAction, transactionPolicy}) => {
+            // Unreported expenses have no report actions yet but are always editable
+            if (!transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
+                return false;
+            }
+            return !canEditFieldOfMoneyRequest({reportAction, fieldToEdit: field, transaction, report, policy: transactionPolicy});
+        });
+
+    const hasPartiallyEditableTransaction = isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.AMOUNT);
+
+    const hasPartiallyEditableMerchantTransaction =
+        isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.MERCHANT) || selectedTransactionContexts.some(({transaction}) => isDistanceRequest(transaction));
+
+    const hasPartiallyEditableTaxRateTransaction =
+        isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.TAX_RATE) || selectedTransactionContexts.some(({transaction}) => isDistanceRequest(transaction));
+
+    const hasPartiallyEditableDateTransaction = isFieldDisabledForAnyTransaction(CONST.EDIT_REQUEST_FIELD.DATE);
+
+    const areSelectedTransactionsBillable = selectedTransactionContexts.every(({transaction, transactionPolicy}) => {
+        // Unreported expenses have no policy yet but billable is always applicable
+        if (!transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
+            return true;
+        }
+        return transactionPolicy?.disabledFields?.defaultBillable === false || !!transaction.billable;
+    });
+
+    const areSelectedTransactionsReimbursable = selectedTransactionContexts.every(({transaction, report, transactionPolicy}) => {
+        // Unreported expenses have no policy yet but reimbursable is always applicable
+        if (!transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
+            return true;
+        }
+        return !isIOUReport(report) && !isInvoiceReport(report) && transactionPolicy?.disabledFields?.reimbursable === false && !isManagedCardTransaction(transaction);
+    });
+
+    const policyID = getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, mergedTransactions, mergedReports);
+
+    const policy = policyID ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : undefined;
+    const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`];
+    const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`];
+    const policyTagLists = getTagLists(policyTags);
+
+    const isTaxTrackingEnabled = !hasPerDiemOrTimeTransaction && selectedTransactionContexts.every(({transactionPolicy}) => !!transactionPolicy?.tax?.trackingEnabled);
+    const areSelectedTransactionsExpenses = selectedTransactionContexts.every(({transaction, report}) => {
+        if (!transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
+            return true;
+        }
+        return !isIOUReport(report);
+    });
+    const areCategoriesEnabled = areSelectedTransactionsExpenses && !!policy?.areCategoriesEnabled && hasEnabledOptions(policyCategories ?? {});
+    const areTagsEnabled = !!policy?.areTagsEnabled && hasEnabledTags(policyTagLists);
+
+    useEffect(() => {
+        return () => {
+            clearBulkEditDraftTransaction();
+        };
+    }, []);
+
+    const save = () => {
+        if (!draftTransaction) {
+            return;
+        }
+
+        const changes: TransactionChanges = {};
+        if (draftTransaction.amount !== undefined) {
+            changes.amount = draftTransaction.amount;
+        }
+        if (draftTransaction.merchant) {
+            changes.merchant = draftTransaction.merchant;
+        }
+        if (draftTransaction.comment?.comment) {
+            changes.comment = draftTransaction.comment.comment;
+        }
+        if (draftTransaction.created) {
+            changes.created = draftTransaction.created;
+        }
+        if (draftTransaction.category) {
+            changes.category = draftTransaction.category;
+        }
+        if (draftTransaction.tag) {
+            changes.tag = draftTransaction.tag;
+        }
+        if (draftTransaction.taxCode) {
+            changes.taxCode = draftTransaction.taxCode;
+        }
+        if (typeof draftTransaction.billable === 'boolean') {
+            changes.billable = draftTransaction.billable;
+        }
+        if (typeof draftTransaction.reimbursable === 'boolean') {
+            changes.reimbursable = draftTransaction.reimbursable;
+        }
+
+        if (Object.keys(changes).length === 0) {
+            Navigation.dismissToPreviousRHP();
+            return;
+        }
+
+        updateMultipleMoneyRequests({
+            transactionIDs: selectedTransactionIDs,
+            changes,
+            policy,
+            reports: mergedReports,
+            transactions: mergedTransactions,
+            reportActions: mergedReportActions,
+            policyCategories: allPolicyCategories,
+            policyTags: allPolicyTags,
+            hash: currentSearchHash,
+            allPolicies: policies,
+        });
+        // Bulk edit can start from report (ID-based selection) or search (map-based selection),
+        // so clear both stores to keep deselection behavior consistent.
+        clearSelectedTransactions(true);
+        clearSelectedTransactions();
+
+        Navigation.dismissToPreviousRHP();
+    };
+
+    const currency = policy?.outputCurrency ?? CONST.CURRENCY.USD;
+
+    // TODO: Currency editing and currency symbol should be handled in a separate PR
+    const tagsArray = getTagArrayFromName(draftTransaction?.tag ?? '');
+    const hasDependentTags = hasDependentTagsPolicyUtils(policy, policyTags);
+    const tagFields: Array<{description: string; title: string; route: Route; disabled?: boolean}> = areTagsEnabled
+        ? policyTagLists.flatMap((tagList, tagListIndex) => {
+              const tagName = tagsArray.at(tagListIndex) ?? '';
+              const tagTitle = tagName ? getCleanedTagName(tagName) : '';
+              const description = tagList.name || translate('common.tag');
+              let shouldShow = true;
+
+              if (hasDependentTags) {
+                  shouldShow = shouldShowDependentTagList(tagListIndex, draftTransaction?.tag, tagList.tags);
+              }
+
+              if (!shouldShow) {
+                  return [];
+              }
+
+              return [
+                  {
+                      description: description || translate('common.tag'),
+                      title: tagTitle,
+                      route: ROUTES.SEARCH_EDIT_MULTIPLE_TAG_RHP.getRoute(tagListIndex),
+                      disabled: false,
+                  },
+              ];
+          })
+        : [];
+
+    const getBooleanTitle = (value?: boolean) => {
+        if (value === undefined) {
+            return '';
+        }
+        return value ? translate('common.yes') : translate('common.no');
+    };
+
+    const fields: Array<{description: string; title: string; route: Route; disabled?: boolean; shouldParseTitle?: boolean}> = [
+        {
+            description: translate('iou.amount'),
+            title: draftTransaction?.amount !== undefined ? convertToDisplayStringWithoutCurrency(draftTransaction.amount, currency) : '',
+            route: ROUTES.SEARCH_EDIT_MULTIPLE_AMOUNT_RHP,
+            disabled: hasCustomUnitTransaction || hasPartiallyEditableTransaction,
+        },
+        {
+            description: translate('common.description'),
+            title: draftTransaction?.comment?.comment ?? '',
+            route: ROUTES.SEARCH_EDIT_MULTIPLE_DESCRIPTION_RHP,
+            shouldParseTitle: true,
+        },
+        {
+            description: translate('common.merchant'),
+            title: draftTransaction?.merchant ?? '',
+            route: ROUTES.SEARCH_EDIT_MULTIPLE_MERCHANT_RHP,
+            disabled: hasPartiallyEditableMerchantTransaction,
+        },
+        {
+            description: translate('common.date'),
+            title: draftTransaction?.created ?? '',
+            route: ROUTES.SEARCH_EDIT_MULTIPLE_DATE_RHP,
+            disabled: hasPartiallyEditableDateTransaction,
+        },
+        ...(areCategoriesEnabled
+            ? [
+                  {
+                      description: translate('common.category'),
+                      title: draftTransaction?.category ?? '',
+                      route: ROUTES.SEARCH_EDIT_MULTIPLE_CATEGORY_RHP,
+                  },
+              ]
+            : []),
+        ...tagFields,
+        ...(isTaxTrackingEnabled
+            ? [
+                  {
+                      description: policy?.taxRates?.name ?? translate('common.tax'),
+                      title: draftTransaction?.taxCode ? (getTaxName(policy, draftTransaction) ?? '') : '',
+                      route: ROUTES.SEARCH_EDIT_MULTIPLE_TAX_RHP,
+                      disabled: hasPartiallyEditableTaxRateTransaction,
+                  },
+              ]
+            : []),
+        ...(areSelectedTransactionsBillable
+            ? [
+                  {
+                      description: translate('common.billable'),
+                      title: getBooleanTitle(draftTransaction?.billable),
+                      route: ROUTES.SEARCH_EDIT_MULTIPLE_BILLABLE_RHP,
+                  },
+              ]
+            : []),
+        ...(areSelectedTransactionsReimbursable
+            ? [
+                  {
+                      description: translate('common.reimbursable'),
+                      title: getBooleanTitle(draftTransaction?.reimbursable),
+                      route: ROUTES.SEARCH_EDIT_MULTIPLE_REIMBURSABLE_RHP,
+                  },
+              ]
+            : []),
+    ];
+
+    return (
+        <ScreenWrapper
+            testID="SearchEditMultiplePage"
+            includeSafeAreaPaddingBottom
+        >
+            <HeaderWithBackButton
+                title={translate('search.bulkActions.editMultipleTitle')}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <View style={[styles.flex1]}>
+                <ScrollView contentContainerStyle={styles.flexGrow1}>
+                    <Text style={[styles.ph5, styles.mb5, styles.textSupporting]}>{translate('search.bulkActions.editMultipleDescription')}</Text>
+                    {fields.map((field) => (
+                        <MenuItemWithTopDescription
+                            key={field.route}
+                            title={field.title}
+                            description={field.description}
+                            onPress={() => Navigation.navigate(field.route)}
+                            shouldShowRightIcon={!field.disabled}
+                            disabled={field.disabled}
+                            interactive={!field.disabled}
+                            shouldParseTitle={field.shouldParseTitle}
+                        />
+                    ))}
+                </ScrollView>
+                <Button
+                    success
+                    large
+                    text={translate('common.save')}
+                    onPress={save}
+                    style={[styles.m5]}
+                />
+            </View>
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultiplePage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleTagPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleTagPage.tsx
new file mode 100644
index 000000000000..b0ded90f6a0d
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleTagPage.tsx
@@ -0,0 +1,84 @@
+import {useRoute} from '@react-navigation/native';
+import React from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TagPicker from '@components/TagPicker';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useSearchBulkEditPolicyID from '@hooks/useSearchBulkEditPolicyID';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import Navigation from '@libs/Navigation/Navigation';
+import {getTagList, hasDependentTags as hasDependentTagsPolicyUtils} from '@libs/PolicyUtils';
+import type {OptionData} from '@libs/ReportUtils';
+import {getUpdatedTransactionTag} from '@libs/TagsOptionsListUtils';
+import {getTagArrayFromName} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {getCommonDependentTag} from './SearchEditMultipleUtils';
+
+function SearchEditMultipleTagPage() {
+    const {translate} = useLocalize();
+    const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+    const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
+    const route = useRoute();
+
+    const selectedTransactionIDs = draftTransaction?.selectedTransactionIDs ?? [];
+
+    const policyID = useSearchBulkEditPolicyID();
+
+    const policy = policyID ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : undefined;
+    const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`);
+
+    const tagListIndex = Number((route.params as {tagListIndex?: string})?.tagListIndex ?? 0);
+    const selectedTransactions = selectedTransactionIDs.map((transactionID) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]);
+    const commonDependentTag = getCommonDependentTag(selectedTransactions);
+    const draftTag = draftTransaction?.tag;
+    const transactionTag = draftTag === undefined ? (commonDependentTag ?? '') : draftTag;
+    const currentTag = getTagArrayFromName(draftTag ?? '').at(tagListIndex) ?? '';
+    const hasDependentTags = hasDependentTagsPolicyUtils(policy, policyTags);
+
+    const tagListName = getTagList(policyTags, tagListIndex).name;
+    const headerTitle = tagListName || translate('common.tag');
+
+    const saveTag = (item: Partial<OptionData>) => {
+        const updatedTag = getUpdatedTransactionTag({
+            transactionTag,
+            selectedTagName: item.searchText ?? '',
+            currentTag,
+            tagListIndex,
+            policyTags,
+            hasDependentTags,
+            hasMultipleTagLists: policy?.hasMultipleTagLists ?? false,
+        });
+
+        updateBulkEditDraftTransaction({
+            tag: updatedTag,
+        });
+        Navigation.goBack();
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID="SearchEditMultipleTagPage"
+        >
+            <HeaderWithBackButton
+                title={headerTitle}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <TagPicker
+                policyID={policyID}
+                selectedTag={currentTag}
+                transactionTag={transactionTag}
+                hasDependentTags={hasDependentTags}
+                tagListName={tagListName}
+                tagListIndex={tagListIndex}
+                onSubmit={saveTag}
+            />
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleTagPage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleTaxPage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultipleTaxPage.tsx
new file mode 100644
index 000000000000..c41c2543171e
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleTaxPage.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import {View} from 'react-native';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TaxPicker from '@components/TaxPicker';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useSearchBulkEditPolicyID from '@hooks/useSearchBulkEditPolicyID';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updateBulkEditDraftTransaction} from '@libs/actions/IOU';
+import Navigation from '@libs/Navigation/Navigation';
+import type {TaxRatesOption} from '@libs/TaxOptionsListUtils';
+import {transformedTaxRates} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+function SearchEditMultipleTaxPage() {
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+    const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
+
+    const policyID = useSearchBulkEditPolicyID();
+
+    const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+    const selectedTaxRate = draftTransaction?.taxCode ? (Object.values(transformedTaxRates(policy)).find((rate) => rate.code === draftTransaction.taxCode)?.modifiedName ?? '') : '';
+
+    const onSubmit = (taxes: TaxRatesOption) => {
+        const nextTaxCode = taxes.code ?? '';
+        const currentTaxCode = draftTransaction?.taxCode ?? '';
+
+        // If the selected tax rate is the same as the current one, clear it
+        if (nextTaxCode === currentTaxCode) {
+            updateBulkEditDraftTransaction({
+                taxCode: null,
+            });
+            Navigation.goBack();
+            return;
+        }
+
+        updateBulkEditDraftTransaction({
+            taxCode: nextTaxCode,
+        });
+        Navigation.goBack();
+    };
+
+    return (
+        <ScreenWrapper
+            includeSafeAreaPaddingBottom
+            shouldEnableMaxHeight
+            testID="SearchEditMultipleTaxPage"
+        >
+            <HeaderWithBackButton
+                title={policy?.taxRates?.name ?? translate('common.tax')}
+                onBackButtonPress={Navigation.goBack}
+            />
+            <View style={[styles.flex1, styles.w100]}>
+                <TaxPicker
+                    selectedTaxRate={selectedTaxRate}
+                    policyID={policyID}
+                    onSubmit={onSubmit}
+                    onDismiss={Navigation.goBack}
+                    action={CONST.IOU.ACTION.EDIT}
+                    iouType={CONST.IOU.TYPE.SUBMIT}
+                    allowDeselect
+                />
+            </View>
+        </ScreenWrapper>
+    );
+}
+
+export default SearchEditMultipleTaxPage;
diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultipleUtils.ts b/src/pages/Search/SearchEditMultiple/SearchEditMultipleUtils.ts
new file mode 100644
index 000000000000..91762dcfcaba
--- /dev/null
+++ b/src/pages/Search/SearchEditMultiple/SearchEditMultipleUtils.ts
@@ -0,0 +1,116 @@
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
+import {getTagArrayFromName} from '@libs/TransactionUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy, Report, ReportActions, SearchResults, Transaction} from '@src/types/onyx';
+
+/**
+ * Returns the longest common dependent tag prefix for the provided transactions.
+ * If there is no shared parent tag, returns undefined.
+ */
+function getCommonDependentTag(transactions: Array<OnyxEntry<Transaction> | undefined>): string | undefined {
+    if (transactions.length === 0) {
+        return;
+    }
+
+    const tagArrays = transactions.map((transaction) => getTagArrayFromName(transaction?.tag ?? ''));
+    if (tagArrays.some((tagArray) => !tagArray.at(0))) {
+        return;
+    }
+
+    const commonTags: string[] = [];
+    const firstTagArray = tagArrays.at(0) ?? [];
+
+    for (let index = 0; index < firstTagArray.length; index++) {
+        const currentTag = firstTagArray.at(index);
+        if (!currentTag) {
+            break;
+        }
+
+        if (tagArrays.every((tagArray) => tagArray.at(index) === currentTag)) {
+            commonTags.push(currentTag);
+            continue;
+        }
+
+        break;
+    }
+
+    return commonTags.length > 0 ? commonTags.join(':') : undefined;
+}
+
+/**
+ * Returns the transaction, report, reportAction, and policy for a given transaction ID.
+ * Returns null if the transaction is not found.
+ */
+function getTransactionEditContext(
+    transactionID: string,
+    allTransactions: OnyxCollection<Transaction> | undefined,
+    allReports: OnyxCollection<Report> | undefined,
+    allReportActions: OnyxCollection<ReportActions> | undefined,
+    policies: OnyxCollection<Policy> | undefined,
+) {
+    const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+    if (!transaction) {
+        return null;
+    }
+    const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
+    const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction.reportID}`] ?? {};
+    const reportAction = getIOUActionForTransactionID(Object.values(reportActions), transactionID);
+    const transactionPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
+    return {transaction, report, reportAction, transactionPolicy};
+}
+
+/**
+ * After a hard refresh, transaction/report/reportAction data may only exist in the search snapshot,
+ * not in the main Onyx collections. These helpers fill gaps from the snapshot so bulk edit can work.
+ */
+function withSnapshotTransactions(onyxTransactions: OnyxCollection<Transaction> | undefined, snapshotData: SearchResults['data'] | undefined): OnyxCollection<Transaction> | undefined {
+    if (!snapshotData) {
+        return onyxTransactions;
+    }
+    const merged = {...onyxTransactions};
+    for (const key of Object.keys(snapshotData)) {
+        if (!key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
+            continue;
+        }
+        const typedKey = key as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`;
+        if (!merged[typedKey]) {
+            merged[typedKey] = snapshotData[typedKey] ?? null;
+        }
+    }
+    return merged;
+}
+
+function withSnapshotReportActions(onyxReportActions: OnyxCollection<ReportActions> | undefined, snapshotData: SearchResults['data'] | undefined): OnyxCollection<ReportActions> | undefined {
+    if (!snapshotData) {
+        return onyxReportActions;
+    }
+    const merged = {...onyxReportActions};
+    for (const key of Object.keys(snapshotData)) {
+        if (!key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS)) {
+            continue;
+        }
+        const typedKey = key as `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`;
+        merged[typedKey] = {...(snapshotData[typedKey] ?? {}), ...(merged[typedKey] ?? {})};
+    }
+    return merged;
+}
+
+function withSnapshotReports(onyxReports: OnyxCollection<Report> | undefined, snapshotData: SearchResults['data'] | undefined): OnyxCollection<Report> | undefined {
+    if (!snapshotData) {
+        return onyxReports;
+    }
+    const merged = {...onyxReports};
+    for (const key of Object.keys(snapshotData)) {
+        if (!key.startsWith(ONYXKEYS.COLLECTION.REPORT) || key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS)) {
+            continue;
+        }
+        const typedKey = key as `${typeof ONYXKEYS.COLLECTION.REPORT}${string}`;
+        if (!merged[typedKey]) {
+            merged[typedKey] = snapshotData[typedKey] ?? null;
+        }
+    }
+    return merged;
+}
+
+export {getCommonDependentTag, getTransactionEditContext, withSnapshotTransactions, withSnapshotReportActions, withSnapshotReports};
diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx
index 01698045f8c0..de4c86062877 100644
--- a/src/pages/iou/request/step/IOURequestStepTag.tsx
+++ b/src/pages/iou/request/step/IOURequestStepTag.tsx
@@ -17,12 +17,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import {setMoneyRequestTag, updateMoneyRequestTag} from '@libs/actions/IOU';
 import {setDraftSplitTransaction} from '@libs/actions/IOU/Split';
 import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
-import {insertTagIntoTransactionTagsString} from '@libs/IOUUtils';
 import Navigation from '@libs/Navigation/Navigation';
-import {getTagList, getTagListName, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isPolicyAdmin} from '@libs/PolicyUtils';
+import {getTagListName, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isPolicyAdmin} from '@libs/PolicyUtils';
 import type {OptionData} from '@libs/ReportUtils';
-import {hasEnabledTags} from '@libs/TagsOptionsListUtils';
-import {getTag, getTagArrayFromName, isPerDiemRequest} from '@libs/TransactionUtils';
+import {getUpdatedTransactionTag, hasEnabledTags} from '@libs/TagsOptionsListUtils';
+import {getTag, isPerDiemRequest} from '@libs/TransactionUtils';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
@@ -95,43 +94,15 @@ function IOURequestStepTag({
     };
 
     const updateTag = (selectedTag: Partial<OptionData>) => {
-        const isSelectedTag = selectedTag.searchText === tag;
-        const searchText = selectedTag.searchText ?? '';
-        let updatedTag: string;
-
-        if (hasDependentTags) {
-            const tagParts = transactionTag ? getTagArrayFromName(transactionTag) : [];
-
-            if (isSelectedTag) {
-                // Deselect: clear this and all child tags
-                tagParts.splice(tagListIndex);
-            } else {
-                // Select new tag: replace this index and clear child tags
-                tagParts.splice(tagListIndex, tagParts.length - tagListIndex, searchText);
-
-                // Check for auto-selection of subsequent tags
-                for (let i = tagListIndex + 1; i < policyTagLists.length; i++) {
-                    const availableNextLevelTags = getTagList(policyTags, i);
-                    const enabledTags = Object.values(availableNextLevelTags.tags).filter((t) => t.enabled);
-
-                    if (enabledTags.length === 1) {
-                        // If there is only one enabled tag, we can auto-select it
-                        const firstTag = enabledTags.at(0);
-                        if (firstTag) {
-                            tagParts.push(firstTag.name);
-                        }
-                    } else {
-                        // If there are no enabled tags or more than one, stop auto-selecting
-                        break;
-                    }
-                }
-            }
-
-            updatedTag = tagParts.join(':');
-        } else {
-            // Independent tags (fallback): use comma-separated list
-            updatedTag = insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : searchText, tagListIndex, policy?.hasMultipleTagLists ?? false);
-        }
+        const updatedTag = getUpdatedTransactionTag({
+            transactionTag,
+            selectedTagName: selectedTag.searchText ?? '',
+            currentTag: tag,
+            tagListIndex,
+            policyTags,
+            hasDependentTags,
+            hasMultipleTagLists: policy?.hasMultipleTagLists ?? false,
+        });
 
         if (isEditingSplit) {
             setDraftSplitTransaction(transactionID, splitDraftTransaction, {tag: updatedTag});
diff --git a/src/types/form/SearchEditMultipleDateForm.ts b/src/types/form/SearchEditMultipleDateForm.ts
new file mode 100644
index 000000000000..031d5e0da328
--- /dev/null
+++ b/src/types/form/SearchEditMultipleDateForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+    DATE: 'date',
+} as const;
+
+type InputID = ValueOf<typeof INPUT_IDS>;
+
+type SearchEditMultipleDateForm = Form<
+    InputID,
+    {
+        [INPUT_IDS.DATE]: string;
+    }
+>;
+
+export type {SearchEditMultipleDateForm};
+export default INPUT_IDS;
diff --git a/src/types/form/SearchEditMultipleDescriptionForm.ts b/src/types/form/SearchEditMultipleDescriptionForm.ts
new file mode 100644
index 000000000000..e723d87489e1
--- /dev/null
+++ b/src/types/form/SearchEditMultipleDescriptionForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+    DESCRIPTION: 'description',
+} as const;
+
+type InputID = ValueOf<typeof INPUT_IDS>;
+
+type SearchEditMultipleDescriptionForm = Form<
+    InputID,
+    {
+        [INPUT_IDS.DESCRIPTION]: string;
+    }
+>;
+
+export type {SearchEditMultipleDescriptionForm};
+export default INPUT_IDS;
diff --git a/src/types/form/SearchEditMultipleMerchantForm.ts b/src/types/form/SearchEditMultipleMerchantForm.ts
new file mode 100644
index 000000000000..ac8baf7c0b39
--- /dev/null
+++ b/src/types/form/SearchEditMultipleMerchantForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+    MERCHANT: 'merchant',
+} as const;
+
+type InputID = ValueOf<typeof INPUT_IDS>;
+
+type SearchEditMultipleMerchantForm = Form<
+    InputID,
+    {
+        [INPUT_IDS.MERCHANT]: string;
+    }
+>;
+
+export type {SearchEditMultipleMerchantForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 964128db0095..7bd554355e2c 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -105,6 +105,9 @@ export type {WorkspacePerDiemForm} from './WorkspacePerDiemForm';
 export type {MoneyRequestRejectReasonForm} from './MoneyRequestRejectReasonForm';
 export type {ReportRejectForm} from './ReportRejectForm';
 export type {EnableGlobalReimbursementsForm} from './EnableGlobalReimbursementsForm';
+export type {SearchEditMultipleDescriptionForm} from './SearchEditMultipleDescriptionForm';
+export type {SearchEditMultipleMerchantForm} from './SearchEditMultipleMerchantForm';
+export type {SearchEditMultipleDateForm} from './SearchEditMultipleDateForm';
 export type {EnterSignerInfoForm} from './EnterSignerInfoForm';
 export type {CreateDomainForm} from './CreateDomainForm';
 export type {SplitExpenseEditDateForm} from './SplitExpenseEditDateForm';
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index d62eeb0aa6c3..93a018a716a5 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -552,6 +552,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
         /** The transaction id */
         transactionID: string;
 
+        /** Selected transaction IDs for bulk edit operations (only used in draft transactions) */
+        selectedTransactionIDs?: string[];
+
         /** The transaction tag */
         tag?: string;
 
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 9d2be74799ac..c17f64a56dd7 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -18,6 +18,7 @@ import {
     cancelPayment,
     canIOUBePaid,
     canUnapproveIOU,
+    clearBulkEditDraftTransaction,
     completePaymentOnboarding,
     convertBulkTrackedExpensesToIOU,
     createDistanceRequest,
@@ -28,6 +29,7 @@ import {
     getReportPreviewAction,
     getTrackExpenseInformation,
     handleNavigateAfterExpenseCreate,
+    initBulkEditDraftTransaction,
     initMoneyRequest,
     markRejectViolationAsResolved,
     payMoneyRequest,
@@ -40,11 +42,13 @@ import {
     shouldOptimisticallyUpdateSearch,
     submitReport,
     trackExpense,
+    updateBulkEditDraftTransaction,
     updateMoneyRequestAmountAndCurrency,
     updateMoneyRequestAttendees,
     updateMoneyRequestCategory,
     updateMoneyRequestDistance,
     updateMoneyRequestTag,
+    updateMultipleMoneyRequests,
 } from '@libs/actions/IOU';
 import {putOnHold} from '@libs/actions/IOU/Hold';
 import {completeSplitBill, splitBill, startSplitBill, updateSplitTransactionsFromSplitExpensesFlow} from '@libs/actions/IOU/Split';
@@ -12026,6 +12030,1244 @@ describe('actions/IOU', () => {
         });
     });
 
+    describe('updateMultipleMoneyRequests', () => {
+        it('applies expense report sign to amount updates', () => {
+            const transactionID = 'transaction-1';
+            const transactionThreadReportID = 'thread-1';
+            const iouReportID = 'iou-1';
+            const policy = createRandomPolicy(1, CONST.POLICY.TYPE.TEAM);
+
+            const transactionThread: Report = {
+                ...createRandomReport(1, undefined),
+                reportID: transactionThreadReportID,
+                parentReportID: iouReportID,
+                policyID: policy.id,
+            };
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction');
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 1000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            const params = writeSpy.mock.calls.at(0)?.[1] as {updates: string};
+            const updates = JSON.parse(params.updates) as {amount: number};
+            expect(updates.amount).toBe(1000);
+            expect(buildOptimisticSpy).toHaveBeenCalledWith(
+                transactionThread,
+                transaction,
+                expect.objectContaining({amount: 1000, currency: CONST.CURRENCY.USD}),
+                true,
+                policy,
+                expect.anything(),
+            );
+
+            writeSpy.mockRestore();
+            buildOptimisticSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('skips updates when bulk edit value matches the current transaction field', () => {
+            const transactionID = 'transaction-1';
+            const transactionThreadReportID = 'thread-1';
+            const iouReportID = 'iou-1';
+            const policy = createRandomPolicy(1, CONST.POLICY.TYPE.TEAM);
+
+            const transactionThread: Report = {
+                ...createRandomReport(1, undefined),
+                reportID: transactionThreadReportID,
+                parentReportID: iouReportID,
+                policyID: policy.id,
+            };
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: -1000,
+                currency: CONST.CURRENCY.USD,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 1000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            expect(writeSpy).not.toHaveBeenCalled();
+
+            writeSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('updates report totals across multiple transactions in the same report', () => {
+            const firstTransactionID = 'transaction-4';
+            const secondTransactionID = 'transaction-5';
+            const iouReportID = 'iou-4';
+            const policy = createRandomPolicy(4, CONST.POLICY.TYPE.TEAM);
+
+            const iouReport: Report = {
+                ...createRandomReport(4, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+                total: -2600,
+                currency: CONST.CURRENCY.USD,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${firstTransactionID}`]: {
+                    ...createRandomTransaction(4),
+                    transactionID: firstTransactionID,
+                    reportID: iouReportID,
+                    amount: -1300,
+                    currency: CONST.CURRENCY.USD,
+                },
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${secondTransactionID}`]: {
+                    ...createRandomTransaction(5),
+                    transactionID: secondTransactionID,
+                    reportID: iouReportID,
+                    amount: -1300,
+                    currency: CONST.CURRENCY.USD,
+                },
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [firstTransactionID, secondTransactionID],
+                changes: {amount: 1000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            const getOptimisticTotal = (callIndex: number) => {
+                const onyxData = writeSpy.mock.calls.at(callIndex)?.[2] as {optimisticData: Array<{key: string; value?: {total?: number}}>};
+                const reportUpdate = onyxData.optimisticData.find((update) => update.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
+                return reportUpdate?.value?.total;
+            };
+
+            expect(getOptimisticTotal(0)).toBe(-2300);
+            expect(getOptimisticTotal(1)).toBe(-2000);
+
+            writeSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('supports negative amount updates for expense reports', () => {
+            const transactionID = 'transaction-3';
+            const transactionThreadReportID = 'thread-3';
+            const iouReportID = 'iou-3';
+            const policy = createRandomPolicy(3, CONST.POLICY.TYPE.TEAM);
+
+            const transactionThread: Report = {
+                ...createRandomReport(3, undefined),
+                reportID: transactionThreadReportID,
+                parentReportID: iouReportID,
+                policyID: policy.id,
+            };
+            const iouReport: Report = {
+                ...createRandomReport(4, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(3),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                // Expense reports store amounts with the opposite sign of what the UI displays.
+                amount: -1000,
+                currency: CONST.CURRENCY.USD,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction');
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: -1000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            const params = writeSpy.mock.calls.at(0)?.[1] as {updates: string};
+            const updates = JSON.parse(params.updates) as {amount: number};
+            expect(updates.amount).toBe(-1000);
+            expect(buildOptimisticSpy).toHaveBeenCalledWith(
+                transactionThread,
+                transaction,
+                expect.objectContaining({amount: -1000, currency: CONST.CURRENCY.USD}),
+                true,
+                policy,
+                expect.anything(),
+            );
+
+            writeSpy.mockRestore();
+            buildOptimisticSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('sends billable and reimbursable updates for bulk edit', () => {
+            const transactionID = 'transaction-7';
+            const transactionThreadReportID = 'thread-7';
+            const iouReportID = 'iou-7';
+            const policy = createRandomPolicy(7, CONST.POLICY.TYPE.TEAM);
+
+            const transactionThread: Report = {
+                ...createRandomReport(7, undefined),
+                reportID: transactionThreadReportID,
+                parentReportID: iouReportID,
+                policyID: policy.id,
+            };
+            const iouReport: Report = {
+                ...createRandomReport(8, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(7),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                billable: false,
+                reimbursable: true,
+                currency: CONST.CURRENCY.USD,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {billable: true, reimbursable: false},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            const params = writeSpy.mock.calls.at(0)?.[1] as {updates: string};
+            const updates = JSON.parse(params.updates) as {billable: boolean; reimbursable: boolean};
+            expect(updates.billable).toBe(true);
+            expect(updates.reimbursable).toBe(false);
+
+            writeSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('keeps invoice amount updates positive', () => {
+            const transactionID = 'transaction-2';
+            const transactionThreadReportID = 'thread-2';
+            const iouReportID = 'iou-2';
+            const policy = createRandomPolicy(2, CONST.POLICY.TYPE.TEAM);
+
+            const transactionThread: Report = {
+                ...createRandomReport(3, undefined),
+                reportID: transactionThreadReportID,
+                parentReportID: iouReportID,
+                policyID: policy.id,
+            };
+            const iouReport: Report = {
+                ...createRandomReport(4, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.INVOICE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(2),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction');
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 1000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            const params = writeSpy.mock.calls.at(0)?.[1] as {updates: string};
+            const updates = JSON.parse(params.updates) as {amount: number};
+            expect(updates.amount).toBe(1000);
+            expect(buildOptimisticSpy).toHaveBeenCalledWith(
+                transactionThread,
+                transaction,
+                expect.objectContaining({amount: 1000, currency: CONST.CURRENCY.USD}),
+                false,
+                policy,
+                expect.anything(),
+            );
+
+            writeSpy.mockRestore();
+            buildOptimisticSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('saves changes for unreported (track) expenses without a reportAction', () => {
+            const transactionID = 'transaction-unreported';
+            const transactionThreadReportID = 'thread-unreported';
+            const policy = createRandomPolicy(10, CONST.POLICY.TYPE.TEAM);
+
+            const transactionThread: Report = {
+                ...createRandomReport(10, undefined),
+                reportID: transactionThreadReportID,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(10),
+                transactionID,
+                reportID: CONST.REPORT.UNREPORTED_REPORT_ID,
+                transactionThreadReportID,
+                amount: 500,
+                currency: CONST.CURRENCY.USD,
+                merchant: 'Old merchant',
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            // No canEditFieldOfMoneyRequest mock — unreported expenses must bypass that check
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {merchant: 'New merchant'},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            expect(writeSpy).toHaveBeenCalled();
+            const params = writeSpy.mock.calls.at(0)?.[1] as {updates: string};
+            const updates = JSON.parse(params.updates) as {merchant: string};
+            expect(updates.merchant).toBe('New merchant');
+
+            writeSpy.mockRestore();
+        });
+
+        it('does not add violations for unreported expenses during bulk edit', async () => {
+            const transactionID = 'transaction-unreported-viol';
+            const transactionThreadReportID = 'thread-unreported-viol';
+            const policy = {...createRandomPolicy(10, CONST.POLICY.TYPE.TEAM), requiresCategory: true, requiresTag: true};
+
+            const transactionThread: Report = {
+                ...createRandomReport(10, undefined),
+                reportID: transactionThreadReportID,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(10),
+                transactionID,
+                reportID: CONST.REPORT.UNREPORTED_REPORT_ID,
+                transactionThreadReportID,
+                amount: 500,
+                currency: CONST.CURRENCY.USD,
+                category: undefined,
+                tag: undefined,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, []);
+            await waitForBatchedUpdates();
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 1000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: {
+                    [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`]: {
+                        // eslint-disable-next-line @typescript-eslint/naming-convention
+                        Food: {name: 'Food', enabled: true, 'GL Code': '', unencodedName: 'Food', externalID: '', areCommentsRequired: false, origin: ''},
+                    },
+                },
+                policyTags: {
+                    [`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy.id}`]: {
+                        Department: {
+                            name: 'Department',
+                            required: true,
+                            orderWeight: 0,
+                            tags: {
+                                // eslint-disable-next-line @typescript-eslint/naming-convention
+                                Engineering: {name: 'Engineering', enabled: true},
+                            },
+                        },
+                    },
+                },
+                hash: undefined,
+            });
+            await waitForBatchedUpdates();
+
+            // Unreported expenses should not get any violations (missingCategory, missingTag, etc.)
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            expect(updatedViolations ?? []).toHaveLength(0);
+        });
+
+        it('removes DUPLICATED_TRANSACTION violation optimistically when amount is changed', async () => {
+            const transactionID = 'transaction-1';
+            const transactionThreadReportID = 'thread-1';
+            const iouReportID = 'iou-1';
+            const policy = createRandomPolicy(1, CONST.POLICY.TYPE.TEAM);
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, [{name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: CONST.VIOLATION_TYPES.VIOLATION}]);
+            await waitForBatchedUpdates();
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 2000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+            await waitForBatchedUpdates();
+
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            const violationNames = updatedViolations?.map((v) => v.name) ?? [];
+            expect(violationNames).not.toContain(CONST.VIOLATIONS.DUPLICATED_TRANSACTION);
+
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('removes CATEGORY_OUT_OF_POLICY violation optimistically when category is cleared', async () => {
+            const transactionID = 'transaction-1';
+            const transactionThreadReportID = 'thread-1';
+            const iouReportID = 'iou-1';
+            const policy = createRandomPolicy(1, CONST.POLICY.TYPE.TEAM);
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+                category: 'OldCategory',
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, [{name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION}]);
+            await waitForBatchedUpdates();
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {category: ''},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+            await waitForBatchedUpdates();
+
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            const violationNames = updatedViolations?.map((v) => v.name) ?? [];
+            expect(violationNames).not.toContain(CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY);
+
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('clears category-out-of-policy violation when the new category is valid', async () => {
+            const transactionID = 'transaction-1';
+            const transactionThreadReportID = 'thread-1';
+            const iouReportID = 'iou-1';
+            const policy = {...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), requiresCategory: true};
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+                category: 'InvalidCategory',
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const policyCategories = {
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                Food: {name: 'Food', enabled: true, 'GL Code': '', unencodedName: 'Food', externalID: '', areCommentsRequired: false, origin: ''},
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, [{name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION}]);
+            await waitForBatchedUpdates();
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {category: 'Food'},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: {
+                    [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`]: policyCategories,
+                },
+                policyTags: {},
+                hash: undefined,
+            });
+            await waitForBatchedUpdates();
+
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            const violationNames = updatedViolations?.map((v) => v.name) ?? [];
+            expect(violationNames).not.toContain(CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY);
+
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('uses the transaction own policy for violation checks in cross-policy bulk edits', async () => {
+            const transactionID = 'transaction-1';
+            const transactionThreadReportID = 'thread-1';
+            const bulkEditPolicyID = '1';
+            const transactionPolicyID = '2';
+
+            // bulkEditPolicy requires categories — would add missingCategory if used for the transaction
+            const bulkEditPolicy = {...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), id: bulkEditPolicyID, requiresCategory: true};
+            // transactionPolicy does NOT require categories — correct policy for this transaction
+            const transactionPolicy = {...createRandomPolicy(2, CONST.POLICY.TYPE.TEAM), id: transactionPolicyID, requiresCategory: false};
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: 'iou-1',
+                policyID: transactionPolicyID,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}iou-1`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: 'iou-1',
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+                category: undefined,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, []);
+            await waitForBatchedUpdates();
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {merchant: 'New Merchant'},
+                policy: bulkEditPolicy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+                allPolicies: {
+                    [`${ONYXKEYS.COLLECTION.POLICY}${transactionPolicyID}`]: transactionPolicy,
+                },
+            });
+            await waitForBatchedUpdates();
+
+            // transactionPolicy does not require categories, so no missingCategory violation should be added
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            const violationNames = updatedViolations?.map((v) => v.name) ?? [];
+            expect(violationNames).not.toContain(CONST.VIOLATIONS.MISSING_CATEGORY);
+
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('does not add false categoryOutOfPolicy violation in cross-policy bulk edit when category exists in transaction policy', async () => {
+            const transactionID = 'transaction-cross-cat-1';
+            const transactionThreadReportID = 'thread-cross-cat-1';
+            const bulkEditPolicyID = 'bulk-policy';
+            const transactionPolicyID = 'tx-policy';
+
+            // bulkEditPolicy does NOT have "Engineering" category
+            const bulkEditPolicy = {...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), id: bulkEditPolicyID, requiresCategory: true};
+            // transactionPolicy DOES have "Engineering" category — the transaction's category is valid here
+            const txPolicy = {...createRandomPolicy(2, CONST.POLICY.TYPE.TEAM), id: transactionPolicyID, requiresCategory: true};
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: 'iou-cross-cat-1',
+                policyID: transactionPolicyID,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}iou-cross-cat-1`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: 'iou-cross-cat-1',
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+                category: 'Engineering',
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, []);
+            await waitForBatchedUpdates();
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+
+            // Pass categories for BOTH policies — "Engineering" only exists in the transaction's policy
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 2000},
+                policy: bulkEditPolicy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: {
+                    [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${bulkEditPolicyID}`]: {
+                        // eslint-disable-next-line @typescript-eslint/naming-convention
+                        Marketing: {name: 'Marketing', enabled: true, 'GL Code': '', unencodedName: 'Marketing', externalID: '', areCommentsRequired: false, origin: ''},
+                    },
+                    [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${transactionPolicyID}`]: {
+                        // eslint-disable-next-line @typescript-eslint/naming-convention
+                        Engineering: {name: 'Engineering', enabled: true, 'GL Code': '', unencodedName: 'Engineering', externalID: '', areCommentsRequired: false, origin: ''},
+                    },
+                },
+                policyTags: {},
+                hash: undefined,
+                allPolicies: {
+                    [`${ONYXKEYS.COLLECTION.POLICY}${transactionPolicyID}`]: txPolicy,
+                },
+            });
+            await waitForBatchedUpdates();
+
+            // "Engineering" exists in the transaction's own policy, so no categoryOutOfPolicy violation
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            const violationNames = updatedViolations?.map((v) => v.name) ?? [];
+            expect(violationNames).not.toContain(CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY);
+
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('adds categoryOutOfPolicy violation when category does not exist in transaction own policy', async () => {
+            const transactionID = 'transaction-bad-cat-1';
+            const transactionThreadReportID = 'thread-bad-cat-1';
+            const policyID = 'cat-policy';
+
+            const policy = {...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), id: policyID, requiresCategory: true};
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: 'iou-bad-cat-1',
+                policyID,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}iou-bad-cat-1`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: 'iou-bad-cat-1',
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+                category: 'NonExistentCategory',
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, []);
+            await waitForBatchedUpdates();
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 2000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: {
+                    [`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]: {
+                        // eslint-disable-next-line @typescript-eslint/naming-convention
+                        Food: {name: 'Food', enabled: true, 'GL Code': '', unencodedName: 'Food', externalID: '', areCommentsRequired: false, origin: ''},
+                    },
+                },
+                policyTags: {},
+                hash: undefined,
+            });
+            await waitForBatchedUpdates();
+
+            // "NonExistentCategory" is not in the policy categories, so categoryOutOfPolicy should be added
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            const violationNames = updatedViolations?.map((v) => v.name) ?? [];
+            expect(violationNames).toContain(CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY);
+
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('uses passed policyTags to detect tagOutOfPolicy violation during bulk edit', async () => {
+            const transactionID = 'transaction-tag-1';
+            const transactionThreadReportID = 'thread-tag-1';
+            const iouReportID = 'iou-tag-1';
+            const policy = {
+                ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM),
+                requiresTag: true,
+                areTagsEnabled: true,
+            };
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+                tag: 'InvalidTag',
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            // Policy tags that do NOT contain "InvalidTag" — only "ValidTag" is enabled
+            const policyTagsForPolicy = {
+                Department: {
+                    name: 'Department',
+                    required: true,
+                    orderWeight: 0,
+                    tags: {
+                        // eslint-disable-next-line @typescript-eslint/naming-convention
+                        ValidTag: {name: 'ValidTag', enabled: true},
+                    },
+                },
+            };
+
+            await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, []);
+            await waitForBatchedUpdates();
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {amount: 2000},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {
+                    [`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy.id}`]: policyTagsForPolicy,
+                },
+                hash: undefined,
+            });
+            await waitForBatchedUpdates();
+
+            // "InvalidTag" is not in the policy tag list, so tagOutOfPolicy should be added
+            const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+            const violationNames = updatedViolations?.map((v) => v.name) ?? [];
+            expect(violationNames).toContain(CONST.VIOLATIONS.TAG_OUT_OF_POLICY);
+
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('skips category, tag, tax, and billable changes for plain IOU transactions', async () => {
+            const transactionID = 'transaction-iou-1';
+            const transactionThreadReportID = 'thread-iou-1';
+            const iouReportID = 'iou-report-1';
+            const policy = createRandomPolicy(1, CONST.POLICY.TYPE.TEAM);
+
+            // IOU report — NOT an expense report (type is not EXPENSE)
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: policy.id,
+                type: CONST.REPORT.TYPE.IOU,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: 1000,
+                currency: CONST.CURRENCY.USD,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {category: 'Food', billable: true},
+                policy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+            });
+
+            // category/billable changes must be silently dropped for IOUs —
+            // no API call should be made since there are no valid updates
+            expect(writeSpy).not.toHaveBeenCalled();
+
+            writeSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('uses per-transaction policy for category tax mapping in cross-policy bulk edit', () => {
+            // Given: two different policies – transactionPolicy has expense rules mapping "Advertising" → "id_TAX_RATE_1",
+            // while the shared bulk-edit policy has no expense rules at all.
+            const transactionID = 'transaction-cross-policy-1';
+            const transactionThreadReportID = 'thread-cross-policy-1';
+            const iouReportID = 'iou-cross-policy-1';
+
+            const category = 'Advertising';
+            const expectedTaxCode = 'id_TAX_RATE_1';
+
+            // Transaction's own policy – has tax expense rules
+            const transactionPolicy: Policy = {
+                ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM),
+                taxRates: CONST.DEFAULT_TAX,
+                rules: {expenseRules: createCategoryTaxExpenseRules(category, expectedTaxCode)},
+            };
+
+            // Shared bulk-edit policy – no expense rules, different ID
+            const sharedBulkEditPolicy: Policy = {
+                ...createRandomPolicy(2, CONST.POLICY.TYPE.TEAM),
+                taxRates: CONST.DEFAULT_TAX,
+                // No expense rules — category should NOT resolve to a tax code via this policy
+            };
+
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: transactionPolicy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: -1000,
+                currency: CONST.CURRENCY.USD,
+                category: '',
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            // Make the transaction's own policy resolvable via allPolicies
+            const allPolicies = {
+                [`${ONYXKEYS.COLLECTION.POLICY}${transactionPolicy.id}`]: transactionPolicy,
+                [`${ONYXKEYS.COLLECTION.POLICY}${sharedBulkEditPolicy.id}`]: sharedBulkEditPolicy,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            // When: bulk-editing with the shared policy (different from transaction's policy)
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {category},
+                policy: sharedBulkEditPolicy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+                allPolicies,
+            });
+
+            // Then: the optimistic transaction update should use the transaction's own policy for tax resolution.
+            // Check the optimistic Onyx data passed to API.write (3rd argument) for the TRANSACTION merge.
+            const writeCall = writeSpy.mock.calls.at(0);
+            expect(writeCall).toBeDefined();
+
+            const onyxData = writeCall?.[2] as {optimisticData: Array<{key: string; value: Partial<Transaction>}>} | undefined;
+            const transactionOnyxUpdate = onyxData?.optimisticData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
+            expect(transactionOnyxUpdate).toBeDefined();
+
+            // The tax code should resolve from the transaction's policy (which has the expense rule),
+            // NOT from the shared bulk-edit policy (which has no expense rules)
+            expect(transactionOnyxUpdate?.value?.taxCode).toBe(expectedTaxCode);
+
+            writeSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+
+        it('passes per-transaction policy to buildOptimisticModifiedExpenseReportAction in cross-policy bulk edit', () => {
+            // Given: two different policies — the transaction belongs to transactionPolicy,
+            // but the shared bulk-edit policy is a different workspace.
+            const transactionID = 'transaction-report-action-policy-1';
+            const transactionThreadReportID = 'thread-report-action-policy-1';
+            const iouReportID = 'iou-report-action-policy-1';
+
+            const transactionPolicy: Policy = {
+                ...createRandomPolicy(10, CONST.POLICY.TYPE.TEAM),
+            };
+
+            const sharedBulkEditPolicy: Policy = {
+                ...createRandomPolicy(20, CONST.POLICY.TYPE.TEAM),
+            };
+
+            const transactionThread: Report = {
+                ...createRandomReport(1, undefined),
+                reportID: transactionThreadReportID,
+                parentReportID: iouReportID,
+                policyID: transactionPolicy.id,
+            };
+            const iouReport: Report = {
+                ...createRandomReport(2, undefined),
+                reportID: iouReportID,
+                policyID: transactionPolicy.id,
+                type: CONST.REPORT.TYPE.EXPENSE,
+                total: 1000,
+            };
+
+            const reports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]: transactionThread,
+                [`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]: iouReport,
+            };
+
+            const transaction: Transaction = {
+                ...createRandomTransaction(1),
+                transactionID,
+                reportID: iouReportID,
+                transactionThreadReportID,
+                amount: -1000,
+                currency: CONST.CURRENCY.USD,
+                reimbursable: true,
+            };
+            const transactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction,
+            };
+
+            const allPolicies = {
+                [`${ONYXKEYS.COLLECTION.POLICY}${transactionPolicy.id}`]: transactionPolicy,
+                [`${ONYXKEYS.COLLECTION.POLICY}${sharedBulkEditPolicy.id}`]: sharedBulkEditPolicy,
+            };
+
+            const canEditFieldSpy = jest.spyOn(require('@libs/ReportUtils'), 'canEditFieldOfMoneyRequest').mockReturnValue(true);
+            const buildOptimisticSpy = jest.spyOn(require('@libs/ReportUtils'), 'buildOptimisticModifiedExpenseReportAction');
+            // eslint-disable-next-line rulesdir/no-multiple-api-calls
+            const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+            // When: bulk-editing reimbursable with the shared policy (different from transaction's policy)
+            updateMultipleMoneyRequests({
+                transactionIDs: [transactionID],
+                changes: {reimbursable: false},
+                policy: sharedBulkEditPolicy,
+                reports,
+                transactions,
+                reportActions: {},
+                policyCategories: undefined,
+                policyTags: {},
+                hash: undefined,
+                allPolicies,
+            });
+
+            // Then: buildOptimisticModifiedExpenseReportAction should receive the transaction's own policy,
+            // not the shared bulk-edit policy. This matters because getUpdatedMoneyRequestReportData
+            // (called after) uses the same policy for maybeUpdateReportNameForFormulaTitle.
+            expect(buildOptimisticSpy).toHaveBeenCalledWith(
+                expect.anything(),
+                expect.anything(),
+                expect.anything(),
+                expect.anything(),
+                expect.objectContaining({id: transactionPolicy.id}),
+                expect.anything(),
+            );
+
+            writeSpy.mockRestore();
+            buildOptimisticSpy.mockRestore();
+            canEditFieldSpy.mockRestore();
+        });
+    });
+
+    describe('bulk edit draft transaction', () => {
+        const draftKey = `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}` as OnyxKey;
+
+        it('initializes the bulk edit draft transaction', async () => {
+            await Onyx.set(draftKey, {amount: 1000});
+            await waitForBatchedUpdates();
+
+            const testTransactionIDs = ['transaction1', 'transaction2', 'transaction3'];
+            initBulkEditDraftTransaction(testTransactionIDs);
+            await waitForBatchedUpdates();
+
+            const draftTransaction = await getOnyxValue(draftKey);
+            expect(draftTransaction).toMatchObject({
+                transactionID: CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID,
+                selectedTransactionIDs: testTransactionIDs,
+            });
+        });
+
+        it('updates the bulk edit draft transaction', async () => {
+            await Onyx.set(draftKey, {transactionID: CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID, merchant: 'Gym'});
+            await waitForBatchedUpdates();
+
+            updateBulkEditDraftTransaction({amount: 1000});
+            await waitForBatchedUpdates();
+
+            const draftTransaction = await getOnyxValue(draftKey);
+            expect(draftTransaction).toMatchObject({
+                transactionID: CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID,
+                merchant: 'Gym',
+                amount: 1000,
+            });
+        });
+
+        it('clears the bulk edit draft transaction', async () => {
+            await Onyx.set(draftKey, {transactionID: CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID, amount: 1000});
+            await waitForBatchedUpdates();
+
+            clearBulkEditDraftTransaction();
+            await waitForBatchedUpdates();
+
+            const draftTransaction = await getOnyxValue(draftKey);
+            expect(draftTransaction).toBeUndefined();
+        });
+    });
+
     describe('cancelPayment', () => {
         const amount = 10000;
         const comment = '💸💸💸💸';
diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts
index fc4e9265d378..861ffd1da10b 100644
--- a/tests/unit/Search/SearchUIUtilsTest.ts
+++ b/tests/unit/Search/SearchUIUtilsTest.ts
@@ -7592,6 +7592,49 @@ describe('SearchUIUtils', () => {
         });
     });
 
+    describe('getSearchBulkEditPolicyID', () => {
+        it('returns the active policy when there are no selected transactions', () => {
+            const result = SearchUIUtils.getSearchBulkEditPolicyID([], 'policy-1', undefined, undefined);
+            expect(result).toBe('policy-1');
+        });
+
+        it('returns the shared policy when all selected transactions match', () => {
+            const transaction1 = {transactionID: 't1', reportID: 'r1'} as OnyxTypes.Transaction;
+            const transaction2 = {transactionID: 't2', reportID: 'r2'} as OnyxTypes.Transaction;
+            const transactions: OnyxCollection<OnyxTypes.Transaction> = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1,
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2,
+            };
+            const bulkReport1 = {reportID: 'r1', policyID: 'policy-1'} as OnyxTypes.Report;
+            const bulkReport2 = {reportID: 'r2', policyID: 'policy-1'} as OnyxTypes.Report;
+            const reports: OnyxCollection<OnyxTypes.Report> = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${bulkReport1.reportID}`]: bulkReport1,
+                [`${ONYXKEYS.COLLECTION.REPORT}${bulkReport2.reportID}`]: bulkReport2,
+            };
+
+            const result = SearchUIUtils.getSearchBulkEditPolicyID(['t1', 't2'], 'policy-2', transactions, reports);
+            expect(result).toBe('policy-1');
+        });
+
+        it('falls back to the active policy when selected transactions are from different policies', () => {
+            const transaction1 = {transactionID: 't1', reportID: 'r1'} as OnyxTypes.Transaction;
+            const transaction2 = {transactionID: 't2', reportID: 'r2'} as OnyxTypes.Transaction;
+            const transactions: OnyxCollection<OnyxTypes.Transaction> = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1,
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2,
+            };
+            const bulkReport1 = {reportID: 'r1', policyID: 'policy-1'} as OnyxTypes.Report;
+            const bulkReport2 = {reportID: 'r2', policyID: 'policy-2'} as OnyxTypes.Report;
+            const reports: OnyxCollection<OnyxTypes.Report> = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${bulkReport1.reportID}`]: bulkReport1,
+                [`${ONYXKEYS.COLLECTION.REPORT}${bulkReport2.reportID}`]: bulkReport2,
+            };
+
+            const result = SearchUIUtils.getSearchBulkEditPolicyID(['t1', 't2'], 'policy-3', transactions, reports);
+            expect(result).toBe('policy-3');
+        });
+    });
+
     describe('shouldShowDeleteOption', () => {
         it('should use the data from selectedTransactions when the data is not available from search result with group-by filter', async () => {
             const TEST_GROUP_KEY = 'group_123456';
diff --git a/tests/unit/SearchEditMultipleUtilsTest.ts b/tests/unit/SearchEditMultipleUtilsTest.ts
new file mode 100644
index 000000000000..6ea82596a1ef
--- /dev/null
+++ b/tests/unit/SearchEditMultipleUtilsTest.ts
@@ -0,0 +1,133 @@
+import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report, SearchResults, Transaction} from '@src/types/onyx';
+import {withSnapshotReports, withSnapshotTransactions} from '../../src/pages/Search/SearchEditMultiple/SearchEditMultipleUtils';
+
+const POLICY_A = 'policyA';
+const POLICY_B = 'policyB';
+const REPORT_ID = 'report1';
+const TRANSACTION_ID_1 = 'tx1';
+const TRANSACTION_ID_2 = 'tx2';
+
+function makeTransaction(transactionID: string, reportID: string): Transaction {
+    return {transactionID, reportID, amount: 100, currency: 'USD', created: '2025-01-01', comment: {}} as Transaction;
+}
+
+function makeReport(reportID: string, policyID: string): Report {
+    return {reportID, policyID, type: 'expense'} as unknown as Report;
+}
+
+describe('SearchEditMultipleUtils', () => {
+    describe('withSnapshotTransactions', () => {
+        it('fills missing transactions from snapshot', () => {
+            const snapshotData = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID),
+            } as unknown as SearchResults['data'];
+
+            const merged = withSnapshotTransactions(undefined, snapshotData);
+
+            expect(merged?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]?.transactionID).toBe(TRANSACTION_ID_1);
+        });
+
+        it('does not overwrite existing transactions', () => {
+            const existing = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, 'existingReport'),
+            };
+            const snapshotData = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, 'snapshotReport'),
+            } as unknown as SearchResults['data'];
+
+            const merged = withSnapshotTransactions(existing, snapshotData);
+
+            expect(merged?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]?.reportID).toBe('existingReport');
+        });
+
+        it('returns original when no snapshot', () => {
+            const existing = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID),
+            };
+
+            const merged = withSnapshotTransactions(existing, undefined);
+
+            expect(merged).toBe(existing);
+        });
+    });
+
+    describe('withSnapshotReports', () => {
+        it('fills missing reports from snapshot', () => {
+            const snapshotData = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A),
+            } as unknown as SearchResults['data'];
+
+            const merged = withSnapshotReports(undefined, snapshotData);
+
+            expect(merged?.[`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]?.policyID).toBe(POLICY_A);
+        });
+
+        it('does not overwrite existing reports', () => {
+            const existing = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A),
+            };
+            const snapshotData = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_B),
+            } as unknown as SearchResults['data'];
+
+            const merged = withSnapshotReports(existing, snapshotData);
+
+            expect(merged?.[`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]?.policyID).toBe(POLICY_A);
+        });
+    });
+
+    describe('getSearchBulkEditPolicyID with snapshot-merged data', () => {
+        it('resolves policyID when report is only in snapshot', () => {
+            // Transaction exists in Onyx, but its report only exists in the snapshot
+            const allTransactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID),
+            };
+            const snapshotData = {
+                [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A),
+            } as unknown as SearchResults['data'];
+
+            // Without snapshot merge — falls back to activePolicyID
+            const withoutMerge = getSearchBulkEditPolicyID([TRANSACTION_ID_1], POLICY_B, allTransactions, undefined);
+            expect(withoutMerge).toBe(POLICY_B);
+
+            // With snapshot merge — resolves to the correct policy
+            const mergedReports = withSnapshotReports(undefined, snapshotData);
+            const withMerge = getSearchBulkEditPolicyID([TRANSACTION_ID_1], POLICY_B, allTransactions, mergedReports);
+            expect(withMerge).toBe(POLICY_A);
+        });
+
+        it('resolves policyID when transaction is only in snapshot', () => {
+            const snapshotData = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, REPORT_ID),
+                [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeReport(REPORT_ID, POLICY_A),
+            } as unknown as SearchResults['data'];
+
+            const mergedTransactions = withSnapshotTransactions(undefined, snapshotData);
+            const mergedReports = withSnapshotReports(undefined, snapshotData);
+
+            const result = getSearchBulkEditPolicyID([TRANSACTION_ID_1], POLICY_B, mergedTransactions, mergedReports);
+            expect(result).toBe(POLICY_A);
+        });
+
+        it('falls back to activePolicyID when transactions span multiple policies', () => {
+            const allTransactions = {
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_1}`]: makeTransaction(TRANSACTION_ID_1, 'reportA'),
+                [`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID_2}`]: makeTransaction(TRANSACTION_ID_2, 'reportB'),
+            };
+            const allReports = {
+                [`${ONYXKEYS.COLLECTION.REPORT}reportA`]: makeReport('reportA', POLICY_A),
+                [`${ONYXKEYS.COLLECTION.REPORT}reportB`]: makeReport('reportB', POLICY_B),
+            };
+
+            const result = getSearchBulkEditPolicyID([TRANSACTION_ID_1, TRANSACTION_ID_2], 'fallback', allTransactions, allReports);
+            expect(result).toBe('fallback');
+        });
+
+        it('returns activePolicyID when no transactions selected', () => {
+            const result = getSearchBulkEditPolicyID([], POLICY_A, undefined, undefined);
+            expect(result).toBe(POLICY_A);
+        });
+    });
+});
diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts
index e5c16b2e9fe8..f16dacfea154 100644
--- a/tests/unit/canEditFieldOfMoneyRequestTest.ts
+++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts
@@ -129,6 +129,22 @@ describe('canEditFieldOfMoneyRequest', () => {
 
                 expect(canEditReportField).toBe(true);
             });
+
+            it('should return false for invoice report action when billable field is edited on an approved invoice report', async () => {
+                await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${IOUReportID}`, {
+                    stateNum: CONST.REPORT.STATE_NUM.APPROVED,
+                    statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
+                });
+                await waitForBatchedUpdates();
+
+                const canEditBillable = canEditFieldOfMoneyRequest({reportAction, fieldToEdit: CONST.EDIT_REQUEST_FIELD.BILLABLE, transaction: moneyRequestTransaction});
+                expect(canEditBillable).toBe(false);
+            });
+
+            it('should return true for invoice report action when billable field is edited on an unapproved invoice report', () => {
+                const canEditBillable = canEditFieldOfMoneyRequest({reportAction, fieldToEdit: CONST.EDIT_REQUEST_FIELD.BILLABLE, transaction: moneyRequestTransaction});
+                expect(canEditBillable).toBe(true);
+            });
         });
 
         describe('type is expense', () => {
diff --git a/tests/unit/canEditMultipleTransactionsTest.ts b/tests/unit/canEditMultipleTransactionsTest.ts
new file mode 100644
index 000000000000..d2706e6a9949
--- /dev/null
+++ b/tests/unit/canEditMultipleTransactionsTest.ts
@@ -0,0 +1,255 @@
+import type {OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import {canEditMultipleTransactions} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {OriginalMessageIOU, Policy, Report, ReportActions, Transaction} from '@src/types/onyx';
+import createRandomPolicy from '../utils/collections/policies';
+import createRandomReportAction from '../utils/collections/reportActions';
+import {createExpenseReport} from '../utils/collections/reports';
+import createRandomTransaction from '../utils/collections/transaction';
+import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
+
+const currentUserAccountID = 1;
+const currentUserEmail = 'test@example.com';
+
+const buildTestData = (options?: {disableSecondAction?: boolean}) => {
+    const transaction1: Transaction = {
+        ...createRandomTransaction(1),
+        transactionID: 't1',
+        reportID: 'r1',
+        amount: 1000,
+        managedCard: false,
+    };
+    const transaction2: Transaction = {
+        ...createRandomTransaction(2),
+        transactionID: 't2',
+        reportID: 'r2',
+        amount: 2000,
+        managedCard: false,
+    };
+    const report1: Report = {
+        ...createExpenseReport(1),
+        reportID: 'r1',
+        policyID: 'p1',
+        stateNum: CONST.REPORT.STATE_NUM.OPEN,
+        statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+    };
+    const report2: Report = {
+        ...createExpenseReport(2),
+        reportID: 'r2',
+        policyID: 'p2',
+        stateNum: CONST.REPORT.STATE_NUM.OPEN,
+        statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+    };
+    const policy1: Policy = {
+        ...createRandomPolicy(1),
+        id: 'p1',
+        role: CONST.POLICY.ROLE.ADMIN,
+    };
+    const policy2: Policy = {
+        ...createRandomPolicy(2),
+        id: 'p2',
+        role: CONST.POLICY.ROLE.ADMIN,
+    };
+    const reportAction1OriginalMessage: OriginalMessageIOU = {
+        IOUReportID: report1.reportID,
+        IOUTransactionID: transaction1.transactionID,
+        type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
+        amount: transaction1.amount,
+        currency: transaction1.currency,
+    };
+    const reportAction1 = {
+        ...createRandomReportAction(1),
+        actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+        actorAccountID: currentUserAccountID,
+        originalMessage: reportAction1OriginalMessage,
+    };
+    const reportAction2OriginalMessage: OriginalMessageIOU = {
+        IOUReportID: report2.reportID,
+        IOUTransactionID: transaction2.transactionID,
+        type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
+        amount: transaction2.amount,
+        currency: transaction2.currency,
+    };
+    const reportAction2 = {
+        ...createRandomReportAction(2),
+        actionName: options?.disableSecondAction ? CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT : CONST.REPORT.ACTIONS.TYPE.IOU,
+        actorAccountID: currentUserAccountID,
+        originalMessage: reportAction2OriginalMessage,
+    };
+
+    const reports: OnyxCollection<Report> = {
+        [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1,
+        [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2,
+    };
+    const policies: OnyxCollection<Policy> = {
+        [`${ONYXKEYS.COLLECTION.POLICY}${policy1.id}`]: policy1,
+        [`${ONYXKEYS.COLLECTION.POLICY}${policy2.id}`]: policy2,
+    };
+    const reportActions: OnyxCollection<ReportActions> = {
+        [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report1.reportID}`]: {
+            [reportAction1.reportActionID]: reportAction1,
+        },
+        [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report2.reportID}`]: {
+            [reportAction2.reportActionID]: reportAction2,
+        },
+    };
+
+    return {
+        transaction1,
+        transaction2,
+        report1,
+        report2,
+        policy1,
+        policy2,
+        reportAction1,
+        reportAction2,
+        reports,
+        policies,
+        reportActions,
+    };
+};
+
+describe('canEditMultipleTransactions', () => {
+    beforeAll(async () => {
+        Onyx.init({keys: ONYXKEYS});
+        await Onyx.set(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail});
+        await waitForBatchedUpdates();
+    });
+
+    afterEach(() => {
+        jest.restoreAllMocks();
+    });
+
+    it('returns false when fewer than two transactions are selected', () => {
+        const {transaction1, reports, policies, reportActions} = buildTestData();
+        const result = canEditMultipleTransactions([transaction1], reportActions, reports, policies);
+
+        expect(result).toBe(false);
+    });
+
+    it('returns false when any selected transaction has no editable fields', () => {
+        const {transaction1, transaction2, reports, policies, reportActions} = buildTestData({disableSecondAction: true});
+
+        const result = canEditMultipleTransactions([transaction1, transaction2], reportActions, reports, policies);
+
+        expect(result).toBe(false);
+    });
+
+    it.each([
+        CONST.IOU.REPORT_ACTION_TYPE.SPLIT,
+        CONST.IOU.REPORT_ACTION_TYPE.REJECT,
+        CONST.IOU.REPORT_ACTION_TYPE.CANCEL,
+        CONST.IOU.REPORT_ACTION_TYPE.DELETE,
+        CONST.IOU.REPORT_ACTION_TYPE.APPROVE,
+    ])('returns false when the IOU action type is %s', (actionType) => {
+        const {transaction1, transaction2, reports, policies, reportActions, reportAction2} = buildTestData();
+
+        reportAction2.originalMessage = {...reportAction2.originalMessage, type: actionType};
+
+        const result = canEditMultipleTransactions([transaction1, transaction2], reportActions, reports, policies);
+
+        expect(result).toBe(false);
+    });
+
+    it('returns false when the IOU action type is pay without IOU details', () => {
+        const {transaction1, transaction2, reports, policies, reportActions, reportAction2} = buildTestData();
+
+        reportAction2.originalMessage = {
+            ...reportAction2.originalMessage,
+            type: CONST.IOU.REPORT_ACTION_TYPE.PAY,
+            IOUDetails: undefined,
+        };
+
+        const result = canEditMultipleTransactions([transaction1, transaction2], reportActions, reports, policies);
+
+        expect(result).toBe(false);
+    });
+
+    it('returns false when the IOU action is deleted', () => {
+        const {transaction1, transaction2, reports, policies, reportActions, reportAction2} = buildTestData();
+
+        reportAction2.originalMessage = {
+            ...reportAction2.originalMessage,
+            deleted: '2024-01-01',
+        };
+
+        const result = canEditMultipleTransactions([transaction1, transaction2], reportActions, reports, policies);
+
+        expect(result).toBe(false);
+    });
+
+    it('returns false when any selected transaction belongs to a reimbursed report', () => {
+        const {transaction1, transaction2, reports, policies, reportActions, report2} = buildTestData();
+
+        report2.statusNum = CONST.REPORT.STATUS_NUM.REIMBURSED;
+
+        const result = canEditMultipleTransactions([transaction1, transaction2], reportActions, reports, policies);
+
+        expect(result).toBe(false);
+    });
+
+    it('returns false when selected entities are reports', () => {
+        const {transaction1, transaction2, reports, policies, reportActions} = buildTestData();
+
+        const result = canEditMultipleTransactions([transaction1, transaction2], reportActions, reports, policies, true);
+
+        expect(result).toBe(false);
+    });
+
+    it('returns true when all selected transactions have an editable field', () => {
+        const {transaction1, transaction2, reports, policies, reportActions} = buildTestData();
+
+        const result = canEditMultipleTransactions([transaction1, transaction2], reportActions, reports, policies);
+
+        expect(result).toBe(true);
+    });
+
+    it('returns true when all selected transactions are unreported expenses', () => {
+        const {transaction1, transaction2, reports, policies, reportActions} = buildTestData();
+
+        const unreportedTransaction1: Transaction = {...transaction1, reportID: CONST.REPORT.UNREPORTED_REPORT_ID};
+        const unreportedTransaction2: Transaction = {...transaction2, reportID: CONST.REPORT.UNREPORTED_REPORT_ID};
+
+        const result = canEditMultipleTransactions([unreportedTransaction1, unreportedTransaction2], reportActions, reports, policies);
+
+        expect(result).toBe(true);
+    });
+
+    it('returns true when some transactions are unreported and others are reported editable expenses', () => {
+        const {transaction1, transaction2, reports, policies, reportActions} = buildTestData();
+
+        const unreportedTransaction2: Transaction = {...transaction2, reportID: CONST.REPORT.UNREPORTED_REPORT_ID};
+
+        const result = canEditMultipleTransactions([transaction1, unreportedTransaction2], reportActions, reports, policies);
+
+        expect(result).toBe(true);
+    });
+
+    it('returns false when selecting an unreported expense and a split expense', () => {
+        const {transaction1, transaction2, reports, policies, reportActions} = buildTestData();
+
+        const unreportedTransaction: Transaction = {...transaction1, reportID: CONST.REPORT.UNREPORTED_REPORT_ID};
+        const splitTransaction: Transaction = {...transaction2, comment: {source: CONST.IOU.TYPE.SPLIT}};
+
+        // Unreported first, split second
+        const result = canEditMultipleTransactions([unreportedTransaction, splitTransaction], reportActions, reports, policies);
+        expect(result).toBe(false);
+
+        // Split first, unreported second
+        const resultReversed = canEditMultipleTransactions([splitTransaction, unreportedTransaction], reportActions, reports, policies);
+        expect(resultReversed).toBe(false);
+    });
+
+    it('returns false when selecting an unreported expense and an approved expense', () => {
+        const {transaction1, transaction2, reports, policies, reportActions, report2} = buildTestData();
+
+        const unreportedTransaction: Transaction = {...transaction1, reportID: CONST.REPORT.UNREPORTED_REPORT_ID};
+        report2.stateNum = CONST.REPORT.STATE_NUM.APPROVED;
+        report2.statusNum = CONST.REPORT.STATUS_NUM.APPROVED;
+
+        const result = canEditMultipleTransactions([unreportedTransaction, transaction2], reportActions, reports, policies);
+        expect(result).toBe(false);
+    });
+});
diff --git a/tests/unit/hooks/useSelectedTransactionsActions.test.ts b/tests/unit/hooks/useSelectedTransactionsActions.test.ts
index 65d302afbdee..f923ffc0aa8d 100644
--- a/tests/unit/hooks/useSelectedTransactionsActions.test.ts
+++ b/tests/unit/hooks/useSelectedTransactionsActions.test.ts
@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/naming-convention */
-import {renderHook, waitFor} from '@testing-library/react-native';
+import {act, renderHook, waitFor} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
 import type {OnyxEntry} from 'react-native-onyx';
 import type {SelectedTransactions} from '@components/Search/types';
@@ -92,6 +92,13 @@ jest.mock('@hooks/useReportIsArchived', () => ({
     default: () => mockIsReportArchived,
 }));
 
+jest.mock('@hooks/usePermissions', () => ({
+    __esModule: true,
+    default: () => ({
+        isBetaEnabled: () => true,
+    }),
+}));
+
 const mockDeleteTransactions = jest.fn(() => []);
 
 jest.mock('@hooks/useDeleteTransactions', () => ({
@@ -192,6 +199,48 @@ describe('useSelectedTransactionsActions', () => {
         expect(exportOption?.text).toBe('common.export');
     });
 
+    it('should show edit multiple option when multiple transactions are editable', async () => {
+        const transactionID1 = '123';
+        const transactionID2 = '456';
+        const report = createRandomReport(1, undefined);
+        const reportActions: ReportAction[] = [];
+        const transaction1 = createRandomTransaction(1);
+        transaction1.transactionID = transactionID1;
+        transaction1.amount = 1000;
+        const transaction2 = createRandomTransaction(2);
+        transaction2.transactionID = transactionID2;
+        transaction2.amount = 2000;
+
+        mockSelectedTransactionIDs.push(transactionID1, transactionID2);
+
+        await act(async () => {
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID1}`, transaction1);
+            await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID2}`, transaction2);
+        });
+
+        jest.spyOn(require('@libs/ReportUtils'), 'canEditMultipleTransactions').mockReturnValue(true);
+
+        const {result} = renderHook(() =>
+            useSelectedTransactionsActions({
+                report,
+                reportActions,
+                allTransactionsLength: 2,
+                beginExportWithTemplate: mockBeginExportWithTemplate,
+            }),
+        );
+
+        await waitFor(() => {
+            expect(result.current.options.length).toBeGreaterThan(0);
+        });
+
+        const editOption = result.current.options.find((option) => option.value === CONST.SEARCH.BULK_ACTION_TYPES.EDIT);
+        expect(editOption).toBeDefined();
+
+        editOption?.onSelected?.();
+
+        expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP);
+    });
+
     it('should handle basic export when online', async () => {
         const transactionID = '123';
         const report = createRandomReport(1, undefined);
