diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png
index 17fce66c073e6528942d7f8de07a95f092b17bd5..4a682c1f0ed07e7f0cd771fe8e89e1439e202cdb 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png
index 9d4020aca1f00f9f5794446dbe29f3f2f8cb4548..f98e9d0f63e8864efa00d4a9be038a01762b653e 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png
index 855fa9c2a100e03771e114c4ddca9dcd241f249c..6e51c3f9faf7d2549527b57d34884efbe9408d28 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png
index a249633846becee32a59dd56ad0853e84b227233..ec8312ca7f3d1219445943bd9ad91dc2f37f3325 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png
index a123a58eb3b9f770e7b6acaec6bd3abfd0a7e2e3..43c1aeca816009a7296098bf3b323233be368dbe 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png
index ba4eda349c303bd08b6eff45cc6aaebc1a2dfe18..7c4ef11d701a599fbad18865309f561a49d370ba 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png
index ee858dd85fb3902c29433cbbba84d01de5374f38..e4b119cf81a25ea436dd31a3aa9ec18eb98bb55e 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png
index 4b2ae366cf5bf7b47b364224bc6adc20a499c561..5d9c61335af4d04b656f6de9e40c245a472fb050 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png differ
diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png
index 5f9e8557af7041453d9082693956f7f8dcc294ae..8fbc99c91f96268502024b6c3b6bad97d947e4bb 100644
Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png differ
diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx
index ecf4f27f7ff487dd3e14713a6009d3ce08cc46ca..beac95b915d34f9b0d833841833580de23bbd7d4 100644
--- a/packages/desktop-client/src/components/accounts/Account.jsx
+++ b/packages/desktop-client/src/components/accounts/Account.jsx
@@ -1,12 +1,10 @@
 import React, { PureComponent, createRef, useMemo } from 'react';
-import { useSelector, useDispatch } from 'react-redux';
+import { useSelector } from 'react-redux';
 import { Navigate, useParams, useLocation, useMatch } from 'react-router-dom';
 
 import { debounce } from 'debounce';
-import { bindActionCreators } from 'redux';
 
 import { validForTransfer } from 'loot-core/client/transfer';
-import * as actions from 'loot-core/src/client/actions';
 import { useFilters } from 'loot-core/src/client/data-hooks/filters';
 import { SchedulesProvider } from 'loot-core/src/client/data-hooks/schedules';
 import * as queries from 'loot-core/src/client/queries';
@@ -26,6 +24,7 @@ import {
 import { applyChanges, groupById } from 'loot-core/src/shared/util';
 
 import { useAccounts } from '../../hooks/useAccounts';
+import { useActions } from '../../hooks/useActions';
 import { useCategories } from '../../hooks/useCategories';
 import { useDateFormat } from '../../hooks/useDateFormat';
 import { useFailedAccounts } from '../../hooks/useFailedAccounts';
@@ -179,7 +178,9 @@ class AccountInternal extends PureComponent {
 
     this.state = {
       search: '',
-      filters: props.conditions || [],
+      filterConditions: props.filterConditions || [],
+      filterId: [],
+      filterConditionsOp: 'and',
       loading: true,
       workingHard: false,
       reconcileAmount: null,
@@ -192,9 +193,8 @@ class AccountInternal extends PureComponent {
       editingName: false,
       isAdding: false,
       latestDate: null,
-      filterId: [],
-      conditionsOp: 'and',
       sort: [],
+      filteredAmount: null,
     };
   }
 
@@ -256,7 +256,7 @@ class AccountInternal extends PureComponent {
     // Important that any async work happens last so that the
     // listeners are set up synchronously
     await this.props.initiallyLoadPayees();
-    await this.fetchTransactions(this.state.filters);
+    await this.fetchTransactions(this.state.filterConditions);
 
     // If there is a pending undo, apply it immediately (this happens
     // when an undo changes the location to this page)
@@ -285,7 +285,7 @@ class AccountInternal extends PureComponent {
 
     //Resest sort/filter/search on account change
     if (this.props.accountId !== prevProps.accountId) {
-      this.setState({ sort: [], search: '', filters: [] });
+      this.setState({ sort: [], search: '', filterConditions: [] });
     }
   }
 
@@ -313,10 +313,10 @@ class AccountInternal extends PureComponent {
     this.paged?.run();
   };
 
-  fetchTransactions = filters => {
+  fetchTransactions = filterConditions => {
     const query = this.makeRootQuery();
     this.rootQuery = this.currentQuery = query;
-    if (filters) this.applyFilters(filters);
+    if (filterConditions) this.applyFilters(filterConditions);
     else this.updateQuery(query);
 
     if (this.props.accountId) {
@@ -371,6 +371,7 @@ class AccountInternal extends PureComponent {
             balances: this.state.showBalances
               ? await this.calculateBalances()
               : null,
+            filteredAmount: await this.getFilteredAmount(),
           },
           () => {
             if (firstLoad) {
@@ -418,7 +419,10 @@ class AccountInternal extends PureComponent {
 
   onSearchDone = debounce(() => {
     if (this.state.search === '') {
-      this.updateQuery(this.currentQuery, this.state.filters.length > 0);
+      this.updateQuery(
+        this.currentQuery,
+        this.state.filterConditions.length > 0,
+      );
     } else {
       this.updateQuery(
         queries.makeTransactionSearchQuery(
@@ -511,7 +515,7 @@ class AccountInternal extends PureComponent {
     return (
       account &&
       this.state.search === '' &&
-      this.state.filters.length === 0 &&
+      this.state.filterConditions.length === 0 &&
       (this.state.sort.length === 0 ||
         (this.state.sort.field === 'date' &&
           this.state.sort.ascDesc === 'desc'))
@@ -599,7 +603,7 @@ class AccountInternal extends PureComponent {
             {
               transactions: [],
               transactionCount: 0,
-              filters: [],
+              filterConditions: [],
               search: '',
               sort: [],
               showBalances: true,
@@ -612,9 +616,9 @@ class AccountInternal extends PureComponent {
         break;
       case 'remove-sorting': {
         this.setState({ sort: [] }, () => {
-          const filters = this.state.filters;
-          if (filters.length > 0) {
-            this.applyFilters([...filters]);
+          const filterConditions = this.state.filterConditions;
+          if (filterConditions.length > 0) {
+            this.applyFilters([...filterConditions]);
           } else {
             this.fetchTransactions();
           }
@@ -637,12 +641,12 @@ class AccountInternal extends PureComponent {
         if (this.state.showReconciled) {
           this.props.savePrefs({ ['hide-reconciled-' + accountId]: true });
           this.setState({ showReconciled: false }, () =>
-            this.fetchTransactions(this.state.filters),
+            this.fetchTransactions(this.state.filterConditions),
           );
         } else {
           this.props.savePrefs({ ['hide-reconciled-' + accountId]: false });
           this.setState({ showReconciled: true }, () =>
-            this.fetchTransactions(this.state.filters),
+            this.fetchTransactions(this.state.filterConditions),
           );
         }
         break;
@@ -680,24 +684,11 @@ class AccountInternal extends PureComponent {
     };
   }
 
-  getFilteredAmount = async (filters, conditionsOpKey) => {
-    const filter = queries.getAccountFilter(this.props.accountId);
-
-    let query = q('transactions').filter({
-      [conditionsOpKey]: [...filters],
-    });
-    if (filter) {
-      query = query.filter(filter);
-    }
-
-    const filteredQuery = await runQuery(
-      query.select([{ amount: { $sum: '$amount' } }]),
-    );
-    const filteredAmount = filteredQuery.data.reduce(
-      (a, v) => (a = a + v.amount),
-      0,
+  getFilteredAmount = async () => {
+    const { data: amount } = await runQuery(
+      this.paged.getQuery().calculate({ $sum: '$amount' }),
     );
-    return filteredAmount;
+    return amount;
   };
 
   isNew = id => {
@@ -817,7 +808,7 @@ class AccountInternal extends PureComponent {
   onShowTransactions = async ids => {
     this.onApplyFilter({
       customName: 'Selected transactions',
-      filter: { id: { $oneof: ids } },
+      queryFilter: { id: { $oneof: ids } },
     });
   };
 
@@ -1209,10 +1200,10 @@ class AccountInternal extends PureComponent {
     );
   };
 
-  onCondOpChange = (value, filters) => {
-    this.setState({ conditionsOp: value });
+  onConditionsOpChange = (value, conditions) => {
+    this.setState({ filterConditionsOp: value });
     this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
-    this.applyFilters([...filters]);
+    this.applyFilters([...conditions]);
     if (this.state.search !== '') {
       this.onSearch(this.state.search);
     }
@@ -1220,14 +1211,14 @@ class AccountInternal extends PureComponent {
 
   onReloadSavedFilter = (savedFilter, item) => {
     if (item === 'reload') {
-      const [getFilter] = this.props.filtersList.filter(
+      const [savedFilter] = this.props.savedFilters.filter(
         f => f.id === this.state.filterId.id,
       );
-      this.setState({ conditionsOp: getFilter.conditionsOp });
-      this.applyFilters([...getFilter.conditions]);
+      this.setState({ filterConditionsOp: savedFilter.conditionsOp });
+      this.applyFilters([...savedFilter.conditions]);
     } else {
       if (savedFilter.status) {
-        this.setState({ conditionsOp: savedFilter.conditionsOp });
+        this.setState({ filterConditionsOp: savedFilter.conditionsOp });
         this.applyFilters([...savedFilter.conditions]);
       }
     }
@@ -1235,7 +1226,7 @@ class AccountInternal extends PureComponent {
   };
 
   onClearFilters = () => {
-    this.setState({ conditionsOp: 'and' });
+    this.setState({ filterConditionsOp: 'and' });
     this.setState({ filterId: [] });
     this.applyFilters([]);
     if (this.state.search !== '') {
@@ -1243,9 +1234,11 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onUpdateFilter = (oldFilter, updatedFilter) => {
+  onUpdateFilter = (oldCondition, updatedCondition) => {
     this.applyFilters(
-      this.state.filters.map(f => (f === oldFilter ? updatedFilter : f)),
+      this.state.filterConditions.map(c =>
+        c === oldCondition ? updatedCondition : c,
+      ),
     );
     this.setState({
       filterId: {
@@ -1258,11 +1251,11 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onDeleteFilter = filter => {
-    this.applyFilters(this.state.filters.filter(f => f !== filter));
-    if (this.state.filters.length === 1) {
+  onDeleteFilter = condition => {
+    this.applyFilters(this.state.filterConditions.filter(c => c !== condition));
+    if (this.state.filterConditions.length === 1) {
       this.setState({ filterId: [] });
-      this.setState({ conditionsOp: 'and' });
+      this.setState({ filterConditionsOp: 'and' });
     } else {
       this.setState({
         filterId: {
@@ -1276,23 +1269,31 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onApplyFilter = async cond => {
-    let filters = this.state.filters;
-    if (cond.customName) {
-      filters = filters.filter(f => f.customName !== cond.customName);
+  onApplyFilter = async conditionOrSavedFilter => {
+    let filterConditions = this.state.filterConditions;
+    if (conditionOrSavedFilter.customName) {
+      filterConditions = filterConditions.filter(
+        c => c.customName !== conditionOrSavedFilter.customName,
+      );
     }
-    if (cond.conditions) {
-      this.setState({ filterId: { ...cond, status: 'saved' } });
-      this.setState({ conditionsOp: cond.conditionsOp });
-      this.applyFilters([...cond.conditions]);
+    if (conditionOrSavedFilter.conditions) {
+      // A saved filter was passed in.
+      const savedFilter = conditionOrSavedFilter;
+      this.setState({
+        filterId: { ...savedFilter, status: 'saved' },
+      });
+      this.setState({ filterConditionsOp: savedFilter.conditionsOp });
+      this.applyFilters([...savedFilter.conditions]);
     } else {
+      // A condition was passed in.
+      const condition = conditionOrSavedFilter;
       this.setState({
         filterId: {
           ...this.state.filterId,
           status: this.state.filterId && 'changed',
         },
       });
-      this.applyFilters([...filters, cond]);
+      this.applyFilters([...filterConditions, condition]);
     }
     if (this.state.search !== '') {
       this.onSearch(this.state.search);
@@ -1320,30 +1321,35 @@ class AccountInternal extends PureComponent {
 
   applyFilters = async conditions => {
     if (conditions.length > 0) {
-      const customFilters = conditions
+      const customQueryFilters = conditions
         .filter(cond => !!cond.customName)
-        .map(f => f.filter);
-      const { filters } = await send('make-filters-from-conditions', {
-        conditions: conditions.filter(cond => !cond.customName),
-      });
-      const conditionsOpKey = this.state.conditionsOp === 'or' ? '$or' : '$and';
-      this.filteredAmount = await this.getFilteredAmount(
-        filters,
-        conditionsOpKey,
+        .map(f => f.queryFilter);
+      const { filters: queryFilters } = await send(
+        'make-filters-from-conditions',
+        {
+          conditions: conditions.filter(cond => !cond.customName),
+        },
       );
+      const conditionsOpKey =
+        this.state.filterConditionsOp === 'or' ? '$or' : '$and';
       this.currentQuery = this.rootQuery.filter({
-        [conditionsOpKey]: [...filters, ...customFilters],
+        [conditionsOpKey]: [...queryFilters, ...customQueryFilters],
       });
 
-      this.setState({ filters: conditions }, () => {
-        this.updateQuery(this.currentQuery, true);
-      });
+      this.setState(
+        {
+          filterConditions: conditions,
+        },
+        () => {
+          this.updateQuery(this.currentQuery, true);
+        },
+      );
     } else {
       this.setState(
         {
           transactions: [],
           transactionCount: 0,
-          filters: conditions,
+          filterConditions: conditions,
         },
         () => {
           this.fetchTransactions();
@@ -1357,8 +1363,8 @@ class AccountInternal extends PureComponent {
   };
 
   applySort = (field, ascDesc, prevField, prevAscDesc) => {
-    const filters = this.state.filters;
-    const isFiltered = filters.length > 0;
+    const filterConditions = this.state.filterConditions;
+    const isFiltered = filterConditions.length > 0;
     const sortField = getField(!field ? this.state.sort.field : field);
     const sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc;
     const sortPrevField = getField(
@@ -1425,7 +1431,7 @@ class AccountInternal extends PureComponent {
       // called directly from UI by sorting a column.
       // active filters need to be applied before sorting
       case isFiltered:
-        this.applyFilters([...filters]);
+        this.applyFilters([...filterConditions]);
         sortCurrentQuery(this, sortField, sortAscDesc);
         break;
 
@@ -1487,7 +1493,6 @@ class AccountInternal extends PureComponent {
       addNotification,
       accountsSyncing,
       failedAccounts,
-      pushModal,
       replaceModal,
       showExtraBalances,
       accountId,
@@ -1505,6 +1510,7 @@ class AccountInternal extends PureComponent {
       balances,
       showCleared,
       showReconciled,
+      filteredAmount,
     } = this.state;
 
     const account = accounts.find(account => account.id === accountId);
@@ -1548,14 +1554,13 @@ class AccountInternal extends PureComponent {
           >
             <View style={styles.page}>
               <AccountHeader
-                filteredAmount={this.filteredAmount}
                 tableRef={this.table}
                 editingName={editingName}
                 isNameEditable={isNameEditable}
                 workingHard={workingHard}
                 account={account}
                 filterId={filterId}
-                filtersList={this.props.filtersList}
+                savedFilters={this.props.savedFilters}
                 location={this.props.location}
                 accountName={accountName}
                 accountsSyncing={accountsSyncing}
@@ -1569,11 +1574,13 @@ class AccountInternal extends PureComponent {
                 showEmptyMessage={showEmptyMessage}
                 balanceQuery={balanceQuery}
                 canCalculateBalance={this.canCalculateBalance}
+                filteredAmount={filteredAmount}
+                isFiltered={transactionsFiltered}
                 isSorted={this.state.sort.length !== 0}
                 reconcileAmount={reconcileAmount}
                 search={this.state.search}
-                filters={this.state.filters}
-                conditionsOp={this.state.conditionsOp}
+                filterConditions={this.state.filterConditions}
+                filterConditionsOp={this.state.filterConditionsOp}
                 savePrefs={this.props.savePrefs}
                 pushModal={this.props.pushModal}
                 onSearch={this.onSearch}
@@ -1598,7 +1605,7 @@ class AccountInternal extends PureComponent {
                 onUpdateFilter={this.onUpdateFilter}
                 onClearFilters={this.onClearFilters}
                 onReloadSavedFilter={this.onReloadSavedFilter}
-                onCondOpChange={this.onCondOpChange}
+                onConditionsOpChange={this.onConditionsOpChange}
                 onDeleteFilter={this.onDeleteFilter}
                 onApplyFilter={this.onApplyFilter}
                 onScheduleAction={this.onScheduleAction}
@@ -1631,9 +1638,7 @@ class AccountInternal extends PureComponent {
                   isAdding={this.state.isAdding}
                   isNew={this.isNew}
                   isMatched={this.isMatched}
-                  isFiltered={
-                    this.state.search !== '' || this.state.filters.length > 0
-                  }
+                  isFiltered={transactionsFiltered}
                   dateFormat={dateFormat}
                   hideFraction={hideFraction}
                   addNotification={addNotification}
@@ -1653,7 +1658,6 @@ class AccountInternal extends PureComponent {
                       </View>
                     ) : null
                   }
-                  pushModal={pushModal}
                   onSort={this.onSort}
                   sortField={this.state.sort.field}
                   ascDesc={this.state.sort.ascDesc}
@@ -1669,6 +1673,7 @@ class AccountInternal extends PureComponent {
                     this.setState({ isAdding: false })
                   }
                   onCreatePayee={this.onCreatePayee}
+                  onApplyFilter={this.onApplyFilter}
                 />
               </View>
             </View>
@@ -1716,36 +1721,10 @@ export function Account() {
   const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
   const accountsSyncing = useSelector(state => state.account.accountsSyncing);
   const lastUndoState = useSelector(state => state.app.lastUndoState);
-  const conditions =
-    location.state && location.state.conditions
-      ? location.state.conditions
-      : [];
-
-  const state = {
-    newTransactions,
-    matchedTransactions,
-    accounts,
-    failedAccounts,
-    dateFormat,
-    hideFraction,
-    expandSplits,
-    showBalances,
-    showCleared: !hideCleared,
-    showReconciled: !hideReconciled,
-    showExtraBalances,
-    payees,
-    modalShowing,
-    accountsSyncing,
-    lastUndoState,
-    conditions,
-  };
+  const filterConditions = location?.state?.filterConditions || [];
 
-  const dispatch = useDispatch();
-  const filtersList = useFilters();
-  const actionCreators = useMemo(
-    () => bindActionCreators(actions, dispatch),
-    [dispatch],
-  );
+  const savedFiters = useFilters();
+  const actionCreators = useActions();
 
   const transform = useMemo(() => {
     const filterByAccount = queries.getAccountFilter(params.id, '_account');
@@ -1774,17 +1753,31 @@ export function Account() {
   return (
     <SchedulesProvider transform={transform}>
       <SplitsExpandedProvider
-        initialMode={state.expandSplits ? 'collapse' : 'expand'}
+        initialMode={expandSplits ? 'collapse' : 'expand'}
       >
         <AccountHack
-          {...state}
+          newTransactions={newTransactions}
+          matchedTransactions={matchedTransactions}
+          accounts={accounts}
+          failedAccounts={failedAccounts}
+          dateFormat={dateFormat}
+          hideFraction={hideFraction}
+          expandSplits={expandSplits}
+          showBalances={showBalances}
+          showCleared={!hideCleared}
+          showReconciled={!hideReconciled}
+          showExtraBalances={showExtraBalances}
+          payees={payees}
+          modalShowing={modalShowing}
+          accountsSyncing={accountsSyncing}
+          lastUndoState={lastUndoState}
+          filterConditions={filterConditions}
           categoryGroups={categoryGroups}
           {...actionCreators}
-          modalShowing={state.modalShowing}
           accountId={params.id}
           categoryId={location?.state?.categoryId}
           location={location}
-          filtersList={filtersList}
+          savedFilters={savedFiters}
         />
       </SplitsExpandedProvider>
     </SchedulesProvider>
diff --git a/packages/desktop-client/src/components/accounts/Balance.jsx b/packages/desktop-client/src/components/accounts/Balance.jsx
index 30706faa17b25b73e7c4f33f62661c3c35f13d0a..f2db1f2e63ab4838838946979cd96d935314085a 100644
--- a/packages/desktop-client/src/components/accounts/Balance.jsx
+++ b/packages/desktop-client/src/components/accounts/Balance.jsx
@@ -137,7 +137,7 @@ export function Balances({
   showExtraBalances,
   onToggleExtraBalances,
   account,
-  filteredItems,
+  isFiltered,
   filteredAmount,
 }) {
   const selectedItems = useSelectedItems();
@@ -196,9 +196,7 @@ export function Balances({
       {selectedItems.size > 0 && (
         <SelectedBalance selectedItems={selectedItems} account={account} />
       )}
-      {filteredItems.length > 0 && (
-        <FilteredBalance filteredAmount={filteredAmount} />
-      )}
+      {isFiltered && <FilteredBalance filteredAmount={filteredAmount} />}
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx
index e152595fa4c4d9436e0b09caac23f67544574dca..fd29b4cc42781c7391e34d7d4e40a2156f039d48 100644
--- a/packages/desktop-client/src/components/accounts/Header.jsx
+++ b/packages/desktop-client/src/components/accounts/Header.jsx
@@ -32,7 +32,6 @@ import { Balances } from './Balance';
 import { ReconcilingMessage, ReconcileMenu } from './Reconcile';
 
 export function AccountHeader({
-  filteredAmount,
   tableRef,
   editingName,
   isNameEditable,
@@ -40,7 +39,7 @@ export function AccountHeader({
   accountName,
   account,
   filterId,
-  filtersList,
+  savedFilters,
   accountsSyncing,
   failedAccounts,
   accounts,
@@ -53,10 +52,12 @@ export function AccountHeader({
   balanceQuery,
   reconcileAmount,
   canCalculateBalance,
+  isFiltered,
+  filteredAmount,
   isSorted,
   search,
-  filters,
-  conditionsOp,
+  filterConditions,
+  filterConditionsOp,
   pushModal,
   onSearch,
   onAddTransaction,
@@ -79,7 +80,7 @@ export function AccountHeader({
   onUpdateFilter,
   onClearFilters,
   onReloadSavedFilter,
-  onCondOpChange,
+  onConditionsOpChange,
   onDeleteFilter,
   onScheduleAction,
   onSetTransfer,
@@ -243,7 +244,7 @@ export function AccountHeader({
           showExtraBalances={showExtraBalances}
           onToggleExtraBalances={onToggleExtraBalances}
           account={account}
-          filteredItems={filters}
+          isFiltered={isFiltered}
           filteredAmount={filteredAmount}
         />
 
@@ -322,7 +323,7 @@ export function AccountHeader({
           )}
           <Button
             type="bare"
-            disabled={search !== '' || filters.length > 0}
+            disabled={search !== '' || filterConditions.length > 0}
             style={{ padding: 6, marginLeft: 10 }}
             onClick={onToggleSplits}
             title={
@@ -391,17 +392,17 @@ export function AccountHeader({
           )}
         </Stack>
 
-        {filters && filters.length > 0 && (
+        {filterConditions?.length > 0 && (
           <FiltersStack
-            filters={filters}
-            conditionsOp={conditionsOp}
+            conditions={filterConditions}
+            conditionsOp={filterConditionsOp}
             onUpdateFilter={onUpdateFilter}
             onDeleteFilter={onDeleteFilter}
             onClearFilters={onClearFilters}
             onReloadSavedFilter={onReloadSavedFilter}
             filterId={filterId}
-            filtersList={filtersList}
-            onCondOpChange={onCondOpChange}
+            savedFilters={savedFilters}
+            onConditionsOpChange={onConditionsOpChange}
           />
         )}
       </View>
diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx
index 67f3ec43c1e2de7083a0629360f5353d11190183..056d240b29802db73f4e5854acaf4f9357c1b948 100644
--- a/packages/desktop-client/src/components/budget/index.tsx
+++ b/packages/desktop-client/src/components/budget/index.tsx
@@ -277,7 +277,7 @@ function BudgetInner(props: BudgetInnerProps) {
   };
 
   const onShowActivity = (categoryId, month) => {
-    const conditions = [
+    const filterConditions = [
       { field: 'category', op: 'is', value: categoryId, type: 'id' },
       {
         field: 'date',
@@ -290,7 +290,7 @@ function BudgetInner(props: BudgetInnerProps) {
     navigate('/accounts', {
       state: {
         goBack: true,
-        conditions,
+        filterConditions,
         categoryId,
       },
     });
diff --git a/packages/desktop-client/src/components/filters/AppliedFilters.tsx b/packages/desktop-client/src/components/filters/AppliedFilters.tsx
index 538bdd72b7ce54b7d27756ad2fd9511dfaee0e33..bb297777e305f3a7ab32a5ec3fb64d32ceefe92a 100644
--- a/packages/desktop-client/src/components/filters/AppliedFilters.tsx
+++ b/packages/desktop-client/src/components/filters/AppliedFilters.tsx
@@ -4,26 +4,29 @@ import { type RuleConditionEntity } from 'loot-core/src/types/models';
 
 import { View } from '../common/View';
 
-import { CondOpMenu } from './CondOpMenu';
+import { ConditionsOpMenu } from './ConditionsOpMenu';
 import { FilterExpression } from './FilterExpression';
 
 type AppliedFiltersProps = {
-  filters: RuleConditionEntity[];
+  conditions: RuleConditionEntity[];
   onUpdate: (
     filter: RuleConditionEntity,
     newFilter: RuleConditionEntity,
   ) => void;
   onDelete: (filter: RuleConditionEntity) => void;
   conditionsOp: string;
-  onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
+  onConditionsOpChange: (
+    value: string,
+    conditions: RuleConditionEntity[],
+  ) => void;
 };
 
 export function AppliedFilters({
-  filters,
+  conditions,
   onUpdate,
   onDelete,
   conditionsOp,
-  onCondOpChange,
+  onConditionsOpChange,
 }: AppliedFiltersProps) {
   return (
     <View
@@ -33,12 +36,12 @@ export function AppliedFilters({
         flexWrap: 'wrap',
       }}
     >
-      <CondOpMenu
+      <ConditionsOpMenu
         conditionsOp={conditionsOp}
-        onCondOpChange={onCondOpChange}
-        filters={filters}
+        onChange={onConditionsOpChange}
+        conditions={conditions}
       />
-      {filters.map((filter: RuleConditionEntity, i: number) => (
+      {conditions.map((filter: RuleConditionEntity, i: number) => (
         <FilterExpression
           key={i}
           customName={filter.customName}
diff --git a/packages/desktop-client/src/components/filters/CondOpMenu.tsx b/packages/desktop-client/src/components/filters/ConditionsOpMenu.tsx
similarity index 67%
rename from packages/desktop-client/src/components/filters/CondOpMenu.tsx
rename to packages/desktop-client/src/components/filters/ConditionsOpMenu.tsx
index af5eec779cb77b54259b100bf526d3a3658ac189..baff43467410c3f946a8265ac7770c80903d4c0d 100644
--- a/packages/desktop-client/src/components/filters/CondOpMenu.tsx
+++ b/packages/desktop-client/src/components/filters/ConditionsOpMenu.tsx
@@ -7,16 +7,16 @@ import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { FieldSelect } from '../modals/EditRule';
 
-export function CondOpMenu({
+export function ConditionsOpMenu({
   conditionsOp,
-  onCondOpChange,
-  filters,
+  onChange,
+  conditions,
 }: {
   conditionsOp: string;
-  onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void;
-  filters: RuleConditionEntity[];
+  onChange: (value: string, conditions: RuleConditionEntity[]) => void;
+  conditions: RuleConditionEntity[];
 }) {
-  return filters.length > 1 ? (
+  return conditions.length > 1 ? (
     <Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
       <FieldSelect
         style={{ display: 'inline-flex' }}
@@ -25,9 +25,7 @@ export function CondOpMenu({
           ['or', 'any'],
         ]}
         value={conditionsOp}
-        onChange={(name: string, value: string) =>
-          onCondOpChange(value, filters)
-        }
+        onChange={(name: string, value: string) => onChange(value, conditions)}
       />
       of:
     </Text>
diff --git a/packages/desktop-client/src/components/filters/FilterExpression.tsx b/packages/desktop-client/src/components/filters/FilterExpression.tsx
index e72a657cb22967591d2b7c597df9e36983d5296c..674be7e8bfc5ca3edd2597d0f5239b2bd7112a14 100644
--- a/packages/desktop-client/src/components/filters/FilterExpression.tsx
+++ b/packages/desktop-client/src/components/filters/FilterExpression.tsx
@@ -61,7 +61,6 @@ export function FilterExpression({
         type="bare"
         disabled={customName != null}
         onClick={() => setEditing(true)}
-        style={{ marginRight: -7 }}
       >
         <div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
           {customName ? (
@@ -76,7 +75,11 @@ export function FilterExpression({
                 value={value}
                 field={field}
                 inline={true}
-                valueIsRaw={op === 'contains' || op === 'doesNotContain'}
+                valueIsRaw={
+                  op === 'contains' ||
+                  op === 'matches' ||
+                  op === 'doesNotContain'
+                }
               />
             </>
           )}
@@ -87,8 +90,7 @@ export function FilterExpression({
           style={{
             width: 8,
             height: 8,
-            margin: 5,
-            marginLeft: 3,
+            margin: 4,
           }}
         />
       </Button>
diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
index ac1f48d6aed7e1eb195ed7f88ac2d248e9b29ba3..1a94aff7944ccf6360062f15f68048c5f4f91bd7 100644
--- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx
+++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
@@ -199,7 +199,8 @@ function ConfigureField({
             field={field}
             subfield={subfield}
             type={
-              type === 'id' && (op === 'contains' || op === 'doesNotContain')
+              type === 'id' &&
+              (op === 'contains' || op === 'matches' || op === 'doesNotContain')
                 ? 'string'
                 : type
             }
diff --git a/packages/desktop-client/src/components/filters/FiltersStack.tsx b/packages/desktop-client/src/components/filters/FiltersStack.tsx
index 3d050ec44a57941506fc7efe336754758d8ff158..3af0c68b736915ef8af9fa34dc48a0f7d746ad5c 100644
--- a/packages/desktop-client/src/components/filters/FiltersStack.tsx
+++ b/packages/desktop-client/src/components/filters/FiltersStack.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { type TransactionFilterEntity } from 'loot-core/types/models';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { Stack } from '../common/Stack';
@@ -12,17 +13,17 @@ import {
 } from './SavedFilterMenuButton';
 
 export function FiltersStack({
-  filters,
+  conditions,
   conditionsOp,
   onUpdateFilter,
   onDeleteFilter,
   onClearFilters,
   onReloadSavedFilter,
   filterId,
-  filtersList,
-  onCondOpChange,
+  savedFilters,
+  onConditionsOpChange,
 }: {
-  filters: RuleConditionEntity[];
+  conditions: RuleConditionEntity[];
   conditionsOp: string;
   onUpdateFilter: (
     filter: RuleConditionEntity,
@@ -32,8 +33,8 @@ export function FiltersStack({
   onClearFilters: () => void;
   onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
   filterId: SavedFilter;
-  filtersList: RuleConditionEntity[];
-  onCondOpChange: () => void;
+  savedFilters: TransactionFilterEntity[];
+  onConditionsOpChange: () => void;
 }) {
   return (
     <View>
@@ -44,20 +45,20 @@ export function FiltersStack({
         align="flex-start"
       >
         <AppliedFilters
-          filters={filters}
+          conditions={conditions}
           conditionsOp={conditionsOp}
-          onCondOpChange={onCondOpChange}
+          onConditionsOpChange={onConditionsOpChange}
           onUpdate={onUpdateFilter}
           onDelete={onDeleteFilter}
         />
         <View style={{ flex: 1 }} />
         <SavedFilterMenuButton
-          filters={filters}
+          conditions={conditions}
           conditionsOp={conditionsOp}
           filterId={filterId}
           onClearFilters={onClearFilters}
           onReloadSavedFilter={onReloadSavedFilter}
-          filtersList={filtersList}
+          savedFilters={savedFilters}
         />
       </Stack>
     </View>
diff --git a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
index 10e1176fa96fb72abb1f997a73b26ec86b3d2750..ba5658f6b76fc3197624e8860e0e23f71b236709 100644
--- a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
+++ b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
@@ -1,6 +1,7 @@
 import React, { useRef, useState } from 'react';
 
 import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
+import { type TransactionFilterEntity } from 'loot-core/types/models';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { SvgExpandArrow } from '../../icons/v0';
@@ -21,19 +22,19 @@ export type SavedFilter = {
 };
 
 export function SavedFilterMenuButton({
-  filters,
+  conditions,
   conditionsOp,
   filterId,
   onClearFilters,
   onReloadSavedFilter,
-  filtersList,
+  savedFilters,
 }: {
-  filters: RuleConditionEntity[];
+  conditions: RuleConditionEntity[];
   conditionsOp: string;
   filterId: SavedFilter;
   onClearFilters: () => void;
   onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
-  filtersList: RuleConditionEntity[];
+  savedFilters: TransactionFilterEntity[];
 }) {
   const [nameOpen, setNameOpen] = useState(false);
   const [adding, setAdding] = useState(false);
@@ -64,7 +65,7 @@ export function SavedFilterMenuButton({
         setAdding(false);
         setMenuOpen(false);
         savedFilter = {
-          conditions: filters,
+          conditions,
           conditionsOp,
           id: filterId.id,
           name: filterId.name,
@@ -72,7 +73,7 @@ export function SavedFilterMenuButton({
         };
         const response = await sendCatch('filter-update', {
           state: savedFilter,
-          filters: [...filtersList],
+          filters: [...savedFilters],
         });
 
         if (response.error) {
@@ -108,7 +109,7 @@ export function SavedFilterMenuButton({
   async function onAddUpdate() {
     if (adding) {
       const newSavedFilter = {
-        conditions: filters,
+        conditions,
         conditionsOp,
         name,
         status: 'saved',
@@ -116,7 +117,7 @@ export function SavedFilterMenuButton({
 
       const response = await sendCatch('filter-create', {
         state: newSavedFilter,
-        filters: [...filtersList],
+        filters: [...savedFilters],
       });
 
       if (response.error) {
@@ -142,7 +143,7 @@ export function SavedFilterMenuButton({
 
     const response = await sendCatch('filter-update', {
       state: updatedFilter,
-      filters: [...filtersList],
+      filters: [...savedFilters],
     });
 
     if (response.error) {
@@ -157,7 +158,7 @@ export function SavedFilterMenuButton({
 
   return (
     <View>
-      {filters.length > 0 && (
+      {conditions.length > 0 && (
         <Button
           ref={triggerRef}
           type="bare"
diff --git a/packages/desktop-client/src/components/filters/updateFilterReducer.ts b/packages/desktop-client/src/components/filters/updateFilterReducer.ts
index 8bbca3ce85055e832bedb59aa25b61a866a43ba9..694c00059228f1ae57ac43c77dd50258db9a819c 100644
--- a/packages/desktop-client/src/components/filters/updateFilterReducer.ts
+++ b/packages/desktop-client/src/components/filters/updateFilterReducer.ts
@@ -12,6 +12,7 @@ export function updateFilterReducer(
       if (
         (type === 'id' || type === 'string') &&
         (action.op === 'contains' ||
+          action.op === 'matches' ||
           action.op === 'is' ||
           action.op === 'doesNotContain' ||
           action.op === 'isNot')
diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx
index 6387eaa11c2b8d9ebdcc7b8cf94853b635d3225a..44617a988645bbce7ab08494d2a5a277718bc5b0 100644
--- a/packages/desktop-client/src/components/modals/EditRule.jsx
+++ b/packages/desktop-client/src/components/modals/EditRule.jsx
@@ -103,10 +103,13 @@ export function OpSelect({
   onChange,
 }) {
   let line;
-  // We don't support the `contains` operator for the id type for
+  // We don't support the `contains, `doesNotContain`, `matches` operators for the id type for
   // rules yet
+  // TODO: Add matches op support for payees, accounts, categories.
   if (type === 'id') {
-    ops = ops.filter(op => op !== 'contains' && op !== 'doesNotContain');
+    ops = ops.filter(
+      op => op !== 'contains' && op !== 'matches' && op !== 'doesNotContain',
+    );
     line = ops.length / 2;
   }
   if (type === 'string') {
diff --git a/packages/desktop-client/src/components/reports/Header.jsx b/packages/desktop-client/src/components/reports/Header.jsx
index 360a754de7a065cb5201f14435ef192807652a93..03c855d29a9b76554fb19908598fa20840ef609c 100644
--- a/packages/desktop-client/src/components/reports/Header.jsx
+++ b/packages/desktop-client/src/components/reports/Header.jsx
@@ -27,7 +27,7 @@ export function Header({
   onApply,
   onUpdateFilter,
   onDeleteFilter,
-  onCondOpChange,
+  onConditionsOpChange,
   headerPrefixItems,
   children,
 }) {
@@ -151,11 +151,11 @@ export function Header({
           align="flex-start"
         >
           <AppliedFilters
-            filters={filters}
+            conditions={filters}
             onUpdate={onUpdateFilter}
             onDelete={onDeleteFilter}
             conditionsOp={conditionsOp}
-            onCondOpChange={onCondOpChange}
+            onConditionsOpChange={onConditionsOpChange}
           />
         </View>
       )}
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
index 57aec0d235ba12ba47922f4c2c363359e98be7f5..c18cf54c9cac902ba1ad8626457776d8876010b7 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
@@ -13,6 +13,7 @@ import { useCategories } from '../../../../hooks/useCategories';
 import { useNavigate } from '../../../../hooks/useNavigate';
 import { useResponsive } from '../../../../ResponsiveProvider';
 import { type CSSProperties, theme } from '../../../../style';
+import { Text } from '../../../common/Text';
 import { View } from '../../../common/View';
 import { Row, Cell } from '../../../table';
 import { showActivity } from '../showActivity';
@@ -125,7 +126,9 @@ export const ReportTableRow = memo(
                     style={{
                       minWidth: compact ? 50 : 85,
                     }}
-                    linkStyle={hoverUnderline}
+                    unexposedContent={({ value }) => (
+                      <Text style={hoverUnderline}>{value}</Text>
+                    )}
                     valueStyle={compactStyle}
                     value={amountToCurrency(intervalItem[balanceTypeOp])}
                     title={
@@ -173,7 +176,9 @@ export const ReportTableRow = memo(
                     style={{
                       minWidth: compact ? 50 : 85,
                     }}
-                    linkStyle={hoverUnderline}
+                    unexposedContent={({ value }) => (
+                      <Text style={hoverUnderline}>{value}</Text>
+                    )}
                     valueStyle={compactStyle}
                     onClick={() =>
                       !isNarrowWidth &&
@@ -208,7 +213,9 @@ export const ReportTableRow = memo(
                     style={{
                       minWidth: compact ? 50 : 85,
                     }}
-                    linkStyle={hoverUnderline}
+                    unexposedContent={({ value }) => (
+                      <Text style={hoverUnderline}>{value}</Text>
+                    )}
                     valueStyle={compactStyle}
                     onClick={() =>
                       !isNarrowWidth &&
@@ -244,7 +251,9 @@ export const ReportTableRow = memo(
               fontWeight: 600,
               minWidth: compact ? 50 : 85,
             }}
-            linkStyle={hoverUnderline}
+            unexposedContent={({ value }) => (
+              <Text style={hoverUnderline}>{value}</Text>
+            )}
             valueStyle={compactStyle}
             onClick={() =>
               !isNarrowWidth &&
diff --git a/packages/desktop-client/src/components/reports/reports/CashFlow.tsx b/packages/desktop-client/src/components/reports/reports/CashFlow.tsx
index 62187004ee24c62f2b56cc0351420ad8c933accd..949f7dd93fd58ab0577bb6956e1ed79ea4a7814f 100644
--- a/packages/desktop-client/src/components/reports/reports/CashFlow.tsx
+++ b/packages/desktop-client/src/components/reports/reports/CashFlow.tsx
@@ -28,12 +28,12 @@ import { useReport } from '../useReport';
 
 export function CashFlow() {
   const {
-    filters,
+    conditions,
     conditionsOp,
     onApply: onApplyFilter,
     onDelete: onDeleteFilter,
     onUpdate: onUpdateFilter,
-    onCondOpChange,
+    onConditionsOpChange,
   } = useFilters<RuleConditionEntity>();
 
   const [allMonths, setAllMonths] = useState<null | Array<{
@@ -55,8 +55,8 @@ export function CashFlow() {
   });
 
   const params = useMemo(
-    () => cashFlowByDate(start, end, isConcise, filters, conditionsOp),
-    [start, end, isConcise, filters, conditionsOp],
+    () => cashFlowByDate(start, end, isConcise, conditions, conditionsOp),
+    [start, end, isConcise, conditions, conditionsOp],
   );
   const data = useReport('cash_flow', params);
 
@@ -129,11 +129,11 @@ export function CashFlow() {
         show1Month
         onChangeDates={onChangeDates}
         onApply={onApplyFilter}
-        filters={filters}
+        filters={conditions}
         onUpdateFilter={onUpdateFilter}
         onDeleteFilter={onDeleteFilter}
         conditionsOp={conditionsOp}
-        onCondOpChange={onCondOpChange}
+        onConditionsOpChange={onConditionsOpChange}
         headerPrefixItems={undefined}
       >
         <View
diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx
index 7171fb521964f7daf2df62dc40c7ac9f15413327..2fb2a375c1b0039f2f114f292eed77ab16f200a5 100644
--- a/packages/desktop-client/src/components/reports/reports/CustomReport.tsx
+++ b/packages/desktop-client/src/components/reports/reports/CustomReport.tsx
@@ -68,12 +68,12 @@ export function CustomReport() {
     useLocalPref('reportsViewLabel');
 
   const {
-    filters,
+    conditions,
     conditionsOp,
     onApply: onApplyFilter,
     onDelete: onDeleteFilter,
     onUpdate: onUpdateFilter,
-    onCondOpChange,
+    onConditionsOpChange,
   } = useFilters();
 
   const location = useLocation();
@@ -260,7 +260,7 @@ export function CustomReport() {
       interval,
       categories,
       selectedCategories,
-      conditions: filters,
+      conditions,
       conditionsOp,
       showEmpty,
       showOffBudget,
@@ -276,7 +276,7 @@ export function CustomReport() {
     balanceTypeOp,
     categories,
     selectedCategories,
-    filters,
+    conditions,
     conditionsOp,
     showEmpty,
     showOffBudget,
@@ -293,7 +293,7 @@ export function CustomReport() {
       interval,
       categories,
       selectedCategories,
-      conditions: filters,
+      conditions,
       conditionsOp,
       showEmpty,
       showOffBudget,
@@ -317,7 +317,7 @@ export function CustomReport() {
     selectedCategories,
     payees,
     accounts,
-    filters,
+    conditions,
     conditionsOp,
     showEmpty,
     showOffBudget,
@@ -349,7 +349,7 @@ export function CustomReport() {
     showUncategorized,
     selectedCategories,
     graphType,
-    conditions: filters,
+    conditions,
     conditionsOp,
   };
 
@@ -494,7 +494,7 @@ export function CustomReport() {
     setGraphType(input.graphType);
     onApplyFilter(null);
     (input.conditions || []).forEach(condition => onApplyFilter(condition));
-    onCondOpChange(input.conditionsOp);
+    onConditionsOpChange(input.conditionsOp);
   };
 
   const onReportChange = ({
@@ -623,7 +623,7 @@ export function CustomReport() {
               defaultItems={defaultItems}
             />
           )}
-          {filters && filters.length > 0 && (
+          {conditions && conditions.length > 0 && (
             <View
               style={{
                 marginBottom: 10,
@@ -635,11 +635,11 @@ export function CustomReport() {
               }}
             >
               <AppliedFilters
-                filters={filters}
+                conditions={conditions}
                 onUpdate={(oldFilter, newFilter) => {
                   setSessionReport(
                     'conditions',
-                    filters.map(f => (f === oldFilter ? newFilter : f)),
+                    conditions.map(f => (f === oldFilter ? newFilter : f)),
                   );
                   onReportChange({ type: 'modify' });
                   onUpdateFilter(oldFilter, newFilter);
@@ -647,14 +647,14 @@ export function CustomReport() {
                 onDelete={deletedFilter => {
                   setSessionReport(
                     'conditions',
-                    filters.filter(f => f !== deletedFilter),
+                    conditions.filter(f => f !== deletedFilter),
                   );
                   onDeleteFilter(deletedFilter);
                   onReportChange({ type: 'modify' });
                 }}
                 conditionsOp={conditionsOp}
-                onCondOpChange={co => {
-                  onCondOpChange(co);
+                onConditionsOpChange={co => {
+                  onConditionsOpChange(co);
                   onReportChange({ type: 'modify' });
                 }}
               />
@@ -704,7 +704,7 @@ export function CustomReport() {
                 {dataCheck ? (
                   <ChooseGraph
                     data={data}
-                    filters={filters}
+                    filters={conditions}
                     mode={mode}
                     graphType={graphType}
                     balanceType={balanceType}
diff --git a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx
index 2e17cae066f362d438e6dac2654a93f51149fff8..1340ba4811211da114037389e460d49b1b0a5682 100644
--- a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx
+++ b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx
@@ -26,13 +26,13 @@ import { fromDateRepr } from '../util';
 export function NetWorth() {
   const accounts = useAccounts();
   const {
-    filters,
+    conditions,
     saved,
     conditionsOp,
     onApply: onApplyFilter,
     onDelete: onDeleteFilter,
     onUpdate: onUpdateFilter,
-    onCondOpChange,
+    onConditionsOpChange,
   } = useFilters();
 
   const [allMonths, setAllMonths] = useState(null);
@@ -42,8 +42,8 @@ export function NetWorth() {
   const [end, setEnd] = useState(monthUtils.currentMonth());
 
   const params = useMemo(
-    () => netWorthSpreadsheet(start, end, accounts, filters, conditionsOp),
-    [start, end, accounts, filters, conditionsOp],
+    () => netWorthSpreadsheet(start, end, accounts, conditions, conditionsOp),
+    [start, end, accounts, conditions, conditionsOp],
   );
   const data = useReport('net_worth', params);
   useEffect(() => {
@@ -108,13 +108,13 @@ export function NetWorth() {
         start={start}
         end={end}
         onChangeDates={onChangeDates}
-        filters={filters}
+        filters={conditions}
         saved={saved}
         onApply={onApplyFilter}
         onUpdateFilter={onUpdateFilter}
         onDeleteFilter={onDeleteFilter}
         conditionsOp={conditionsOp}
-        onCondOpChange={onCondOpChange}
+        onConditionsOpChange={onConditionsOpChange}
       />
 
       <View
diff --git a/packages/desktop-client/src/components/reports/reports/Spending.tsx b/packages/desktop-client/src/components/reports/reports/Spending.tsx
index 5f54faaaf7823f8adbf38c1849203a4b27a913f5..bfecccbd13ea30264e14a29b06ff9fdde98779a6 100644
--- a/packages/desktop-client/src/components/reports/reports/Spending.tsx
+++ b/packages/desktop-client/src/components/reports/reports/Spending.tsx
@@ -29,12 +29,12 @@ export function Spending() {
   const categories = useCategories();
 
   const {
-    filters,
+    conditions,
     conditionsOp,
     onApply: onApplyFilter,
     onDelete: onDeleteFilter,
     onUpdate: onUpdateFilter,
-    onCondOpChange,
+    onConditionsOpChange,
   } = useFilters<RuleConditionEntity>();
 
   const [dataCheck, setDataCheck] = useState(false);
@@ -44,11 +44,11 @@ export function Spending() {
     setDataCheck(false);
     return createSpendingSpreadsheet({
       categories,
-      conditions: filters,
+      conditions,
       conditionsOp,
       setDataCheck,
     });
-  }, [categories, filters, conditionsOp]);
+  }, [categories, conditions, conditionsOp]);
 
   const data = useReport('default', getGraphData);
   const navigate = useNavigate();
@@ -100,7 +100,7 @@ export function Spending() {
             flexShrink: 0,
           }}
         >
-          {filters && (
+          {conditions && (
             <View style={{ flexDirection: 'row' }}>
               <FilterButton
                 onApply={onApplyFilter}
@@ -126,7 +126,7 @@ export function Spending() {
             flexGrow: 1,
           }}
         >
-          {filters && filters.length > 0 && (
+          {conditions && conditions.length > 0 && (
             <View
               style={{
                 marginBottom: 10,
@@ -139,11 +139,11 @@ export function Spending() {
               }}
             >
               <AppliedFilters
-                filters={filters}
+                conditions={conditions}
                 onUpdate={onUpdateFilter}
                 onDelete={onDeleteFilter}
                 conditionsOp={conditionsOp}
-                onCondOpChange={onCondOpChange}
+                onConditionsOpChange={onConditionsOpChange}
               />
             </View>
           )}
diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx
index 42560177b3654363ba4498d00aa9fe52cbbf20f4..81516a418db02f684c4460aab7521b89440939c0 100644
--- a/packages/desktop-client/src/components/table.tsx
+++ b/packages/desktop-client/src/components/table.tsx
@@ -114,8 +114,9 @@ export const Field = forwardRef<HTMLDivElement, FieldProps>(function Field(
 export function UnexposedCellContent({
   value,
   formatter,
-  linkStyle,
-}: Pick<CellProps, 'value' | 'formatter' | 'linkStyle'>) {
+  style,
+  ...props
+}: Pick<CellProps, 'value' | 'formatter' | 'style'>) {
   return (
     <Text
       style={{
@@ -123,7 +124,8 @@ export function UnexposedCellContent({
         whiteSpace: 'nowrap',
         overflow: 'hidden',
         textOverflow: 'ellipsis',
-        ...linkStyle,
+        ...style,
+        ...props,
       }}
     >
       {formatter ? formatter(value) : value}
@@ -139,10 +141,11 @@ type CellProps = Omit<ComponentProps<typeof View>, 'children' | 'value'> & {
   plain?: boolean;
   exposed?: boolean;
   children?: ReactNode | (() => ReactNode);
-  unexposedContent?: ReactNode;
+  unexposedContent?: (
+    props: ComponentProps<typeof UnexposedCellContent>,
+  ) => ReactNode;
   value?: string;
   valueStyle?: CSSProperties;
-  linkStyle?: CSSProperties;
   onExpose?: (name: string) => void;
   privacyFilter?: ComponentProps<
     typeof ConditionalPrivacyFilter
@@ -162,7 +165,6 @@ export function Cell({
   plain,
   style,
   valueStyle,
-  linkStyle,
   unexposedContent,
   privacyFilter,
   ...viewProps
@@ -233,12 +235,10 @@ export function Cell({
                   }
             }
           >
-            {unexposedContent || (
-              <UnexposedCellContent
-                linkStyle={linkStyle}
-                value={value}
-                formatter={formatter}
-              />
+            {unexposedContent ? (
+              unexposedContent({ value, formatter })
+            ) : (
+              <UnexposedCellContent value={value} formatter={formatter} />
             )}
           </View>
         )}
diff --git a/packages/desktop-client/src/components/transactions/TransactionList.jsx b/packages/desktop-client/src/components/transactions/TransactionList.jsx
index c25778a34e3a3be45e66542ce2639bb998bcedcf..20cf3a97717421818cfb40eea3c4d2621cb7814f 100644
--- a/packages/desktop-client/src/components/transactions/TransactionList.jsx
+++ b/packages/desktop-client/src/components/transactions/TransactionList.jsx
@@ -1,5 +1,9 @@
 import React, { useRef, useCallback, useLayoutEffect } from 'react';
+import { useDispatch } from 'react-redux';
 
+import escapeRegExp from 'lodash/escapeRegExp';
+
+import { pushModal } from 'loot-core/client/actions';
 import { send } from 'loot-core/src/platform/client/fetch';
 import {
   splitTransaction,
@@ -76,7 +80,6 @@ export function TransactionList({
   dateFormat,
   hideFraction,
   addNotification,
-  pushModal,
   renderEmpty,
   onSort,
   sortField,
@@ -85,9 +88,11 @@ export function TransactionList({
   onRefetch,
   onCloseAddTransaction,
   onCreatePayee,
+  onApplyFilter,
 }) {
   const transactionsLatest = useRef();
   const navigate = useNavigate();
+  const dispatch = useDispatch();
 
   useLayoutEffect(() => {
     transactionsLatest.current = transactions;
@@ -161,13 +166,21 @@ export function TransactionList({
   });
 
   const onNavigateToSchedule = useCallback(scheduleId => {
-    pushModal('schedule-edit', { id: scheduleId });
+    dispatch(pushModal('schedule-edit', { id: scheduleId }));
+  });
+
+  const onNotesTagClick = useCallback(tag => {
+    onApplyFilter({
+      field: 'notes',
+      op: 'matches',
+      value: `(^|\\s|\\w|#)${escapeRegExp(tag)}($|\\s|#)`,
+      type: 'string',
+    });
   });
 
   return (
     <TransactionTable
       ref={tableRef}
-      pushModal={pushModal}
       transactions={allTransactions}
       loadMoreTransactions={loadMoreTransactions}
       accounts={accounts}
@@ -200,6 +213,7 @@ export function TransactionList({
       style={{ backgroundColor: theme.tableBackground }}
       onNavigateToTransferAccount={onNavigateToTransferAccount}
       onNavigateToSchedule={onNavigateToSchedule}
+      onNotesTagClick={onNotesTagClick}
       onSort={onSort}
       sortField={sortField}
       ascDesc={ascDesc}
diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx
index 46849be920ff686953187a6de744757429cbe65d..60286ed4eca32efc28656edbff4d8d800b253936 100644
--- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx
+++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx
@@ -10,6 +10,7 @@ import React, {
   useLayoutEffect,
   useEffect,
 } from 'react';
+import { useDispatch } from 'react-redux';
 
 import {
   format as formatDate,
@@ -17,6 +18,7 @@ import {
   isValid as isDateValid,
 } from 'date-fns';
 
+import { pushModal } from 'loot-core/client/actions';
 import { useCachedSchedules } from 'loot-core/src/client/data-hooks/schedules';
 import {
   getAccountsById,
@@ -413,7 +415,8 @@ function HeaderCell({
       width={width}
       name={id}
       alignItems={alignItems}
-      unexposedContent={
+      value={value}
+      unexposedContent={({ value: cellValue }) => (
         <Button
           type="bare"
           onClick={onClick}
@@ -427,7 +430,7 @@ function HeaderCell({
             marginRight,
           }}
         >
-          <UnexposedCellContent value={value} />
+          <UnexposedCellContent value={cellValue} />
           {icon === 'asc' && (
             <SvgArrowDown width={10} height={10} style={{ marginLeft: 5 }} />
           )}
@@ -435,22 +438,20 @@ function HeaderCell({
             <SvgArrowUp width={10} height={10} style={{ marginLeft: 5 }} />
           )}
         </Button>
-      }
+      )}
     />
   );
 }
 
 function PayeeCell({
   id,
-  payeeId,
-  accountId,
+  payee,
   focused,
   inherited,
   payees,
   accounts,
   valueStyle,
   transaction,
-  payee,
   transferAcct,
   isPreview,
   onEdit,
@@ -462,16 +463,12 @@ function PayeeCell({
 }) {
   const isCreatingPayee = useRef(false);
 
-  // Filter out the account we're currently in as it is not a valid transfer
-  accounts = accounts.filter(account => account.id !== accountId);
-  payees = payees.filter(payee => payee.transfer_acct !== accountId);
-
   return (
     <CustomCell
       width="flex"
       name="payee"
       textAlign="flex"
-      value={payeeId}
+      value={payee?.id}
       valueStyle={{
         ...valueStyle,
         ...(inherited && { color: theme.tableTextInactive }),
@@ -488,7 +485,8 @@ function PayeeCell({
           isCreatingPayee.current = false;
         }
       }}
-      unexposedContent={
+      formatter={() => getPayeePretty(transaction, payee, transferAcct)}
+      unexposedContent={props => (
         <>
           <PayeeIcons
             transaction={transaction}
@@ -496,12 +494,9 @@ function PayeeCell({
             onNavigateToTransferAccount={onNavigateToTransferAccount}
             onNavigateToSchedule={onNavigateToSchedule}
           />
-          <UnexposedCellContent
-            value={payeeId}
-            formatter={() => getPayeePretty(transaction, payee, transferAcct)}
-          />
+          <UnexposedCellContent {...props} />
         </>
-      }
+      )}
     >
       {({
         onBlur,
@@ -515,7 +510,7 @@ function PayeeCell({
           <PayeeAutocomplete
             payees={payees}
             accounts={accounts}
-            value={payeeId}
+            value={payee?.id}
             shouldSaveFromKey={shouldSaveFromKey}
             inputProps={{
               onBlur,
@@ -527,7 +522,7 @@ function PayeeCell({
             focused={true}
             onUpdate={(id, value) => onUpdate?.(value)}
             onSelect={onSave}
-            onManagePayees={() => onManagePayees(payeeId)}
+            onManagePayees={() => onManagePayees(payee?.id)}
             menuPortalTarget={undefined}
           />
         );
@@ -641,8 +636,10 @@ const Transaction = memo(function Transaction(props) {
     onToggleSplit,
     onNavigateToTransferAccount,
     onNavigateToSchedule,
+    onNotesTagClick,
   } = props;
 
+  const dispatch = useDispatch();
   const dispatchSelected = useSelectedDispatch();
 
   const [prevShowZero, setPrevShowZero] = useState(showZeroInDeposit);
@@ -685,13 +682,15 @@ const Transaction = memo(function Transaction(props) {
       ) {
         if (showReconciliationWarning === false) {
           setShowReconciliationWarning(true);
-          props.pushModal('confirm-transaction-edit', {
-            onConfirm: () => {
-              setShowReconciliationWarning(false);
-              onUpdateAfterConfirm(name, value);
-            },
-            confirmReason: 'editReconciled',
-          });
+          dispatch(
+            pushModal('confirm-transaction-edit', {
+              onConfirm: () => {
+                setShowReconciliationWarning(false);
+                onUpdateAfterConfirm(name, value);
+              },
+              confirmReason: 'editReconciled',
+            }),
+          );
         }
       } else {
         onUpdateAfterConfirm(name, value);
@@ -700,12 +699,14 @@ const Transaction = memo(function Transaction(props) {
 
     // Allow un-reconciling (unlocking) transactions
     if (name === 'cleared' && transaction.reconciled) {
-      props.pushModal('confirm-transaction-edit', {
-        onConfirm: () => {
-          onUpdateAfterConfirm('reconciled', false);
-        },
-        confirmReason: 'unlockReconciled',
-      });
+      dispatch(
+        pushModal('confirm-transaction-edit', {
+          onConfirm: () => {
+            onUpdateAfterConfirm('reconciled', false);
+          },
+          confirmReason: 'unlockReconciled',
+        }),
+      );
     }
   }
 
@@ -976,15 +977,14 @@ const Transaction = memo(function Transaction(props) {
         <PayeeCell
           /* Payee field for all transactions */
           id={id}
-          payeeId={payeeId}
-          accountId={accountId}
+          payee={payee}
           focused={focusedField === 'payee'}
           inherited={inheritedFields && inheritedFields.has('payee')}
-          payees={payees}
-          accounts={accounts}
+          /* Filter out the account we're currently in as it is not a valid transfer */
+          accounts={accounts.filter(account => account.id !== accountId)}
+          payees={payees.filter(payee => payee.transfer_acct !== accountId)}
           valueStyle={valueStyle}
           transaction={transaction}
-          payee={payee}
           transferAcct={transferAcct}
           importedPayee={importedPayee}
           isPreview={isPreview}
@@ -1009,6 +1009,7 @@ const Transaction = memo(function Transaction(props) {
           focused={focusedField === 'notes'}
           value={notes || ''}
           valueStyle={valueStyle}
+          formatter={value => notesTagFormatter(value, onNotesTagClick)}
           onExpose={name => !isPreview && onEdit(id, name)}
           inputProps={{
             value: notes || '',
@@ -1391,6 +1392,7 @@ function NewTransaction({
   onCreatePayee,
   onNavigateToTransferAccount,
   onNavigateToSchedule,
+  onNotesTagClick,
   balance,
 }) {
   const error = transactions[0].error;
@@ -1444,6 +1446,7 @@ function NewTransaction({
           style={{ marginTop: -1 }}
           onNavigateToTransferAccount={onNavigateToTransferAccount}
           onNavigateToSchedule={onNavigateToSchedule}
+          onNotesTagClick={onNotesTagClick}
           balance={balance}
         />
       ))}
@@ -1523,6 +1526,14 @@ function TransactionTableInner({
     [props.onCloseAddTransaction, props.onNavigateToSchedule],
   );
 
+  const onNotesTagClick = useCallback(
+    noteTag => {
+      props.onCloseAddTransaction();
+      props.onNotesTagClick(noteTag);
+    },
+    [props.onCloseAddTransaction, props.onNotesTagClick],
+  );
+
   useEffect(() => {
     if (!isAddingPrev && props.isAdding) {
       newNavigator.onEdit('temp', 'date');
@@ -1625,7 +1636,7 @@ function TransactionTableInner({
           onToggleSplit={props.onToggleSplit}
           onNavigateToTransferAccount={onNavigateToTransferAccount}
           onNavigateToSchedule={onNavigateToSchedule}
-          pushModal={props.pushModal}
+          onNotesTagClick={onNotesTagClick}
         />
       </>
     );
@@ -1683,6 +1694,7 @@ function TransactionTableInner({
               onCreatePayee={props.onCreatePayee}
               onNavigateToTransferAccount={onNavigateToTransferAccount}
               onNavigateToSchedule={onNavigateToSchedule}
+              onNotesTagClick={onNotesTagClick}
               onDistributeRemainder={props.onDistributeRemainder}
               balance={
                 props.transactions?.length > 0
@@ -2171,3 +2183,59 @@ export const TransactionTable = forwardRef((props, ref) => {
 });
 
 TransactionTable.displayName = 'TransactionTable';
+
+function notesTagFormatter(notes, onNotesTagClick) {
+  const words = notes.split(' ');
+  return (
+    <>
+      {words.map((word, i, arr) => {
+        const separator = arr.length - 1 === i ? '' : ' ';
+        if (word.includes('#') && word.length > 1) {
+          // Treat tags in a single word as separate tags.
+          // #tag1#tag2 => (#tag1)(#tag2)
+          // not-a-tag#tag2#tag3 => not-a-tag(#tag2)(#tag3)
+          return word.split('#').map((tag, ti) => {
+            if (ti === 0) {
+              return tag;
+            }
+
+            if (!tag) {
+              return '#';
+            }
+
+            const validTag = `#${tag}`;
+            return (
+              <span key={`${validTag}${ti}`}>
+                <Button
+                  type="bare"
+                  key={i}
+                  style={{
+                    display: 'inline-flex',
+                    padding: '3px 7px',
+                    borderRadius: 16,
+                    userSelect: 'none',
+                    backgroundColor: theme.noteTagBackground,
+                    color: theme.noteTagText,
+                    cursor: 'pointer',
+                  }}
+                  hoveredStyle={{
+                    backgroundColor: theme.noteTagBackgroundHover,
+                    color: theme.noteTagText,
+                  }}
+                  onClick={e => {
+                    e.stopPropagation();
+                    onNotesTagClick?.(validTag);
+                  }}
+                >
+                  {validTag}
+                </Button>
+                {separator}
+              </span>
+            );
+          });
+        }
+        return `${word}${separator}`;
+      })}
+    </>
+  );
+}
diff --git a/packages/desktop-client/src/hooks/useFilters.ts b/packages/desktop-client/src/hooks/useFilters.ts
index cd8fcaed8688a75b0d90c1b36f8f2f767f808124..825b0e2cb3b2ecadf7c5f42ee9938e4b14e93cc9 100644
--- a/packages/desktop-client/src/hooks/useFilters.ts
+++ b/packages/desktop-client/src/hooks/useFilters.ts
@@ -4,48 +4,48 @@ import { useCallback, useMemo, useState } from 'react';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 export function useFilters<T extends RuleConditionEntity>(
-  initialFilters: T[] = [],
+  initialConditions: T[] = [],
 ) {
-  const [filters, setFilters] = useState<T[]>(initialFilters);
+  const [conditions, setConditions] = useState<T[]>(initialConditions);
   const [conditionsOp, setConditionsOp] = useState<'and' | 'or'>('and');
   const [saved, setSaved] = useState<T[]>(null);
 
   const onApply = useCallback(
-    newFilter => {
-      if (newFilter === null) {
-        setFilters([]);
+    conditionsOrSavedFilter => {
+      if (conditionsOrSavedFilter === null) {
+        setConditions([]);
         setSaved(null);
-      } else if (newFilter.conditions) {
-        setFilters([...newFilter.conditions]);
-        setConditionsOp(newFilter.conditionsOp);
-        setSaved(newFilter.id);
+      } else if (conditionsOrSavedFilter.conditions) {
+        setConditions([...conditionsOrSavedFilter.conditions]);
+        setConditionsOp(conditionsOrSavedFilter.conditionsOp);
+        setSaved(conditionsOrSavedFilter.id);
       } else {
-        setFilters(state => [...state, newFilter]);
+        setConditions(state => [...state, conditionsOrSavedFilter]);
         setSaved(null);
       }
     },
-    [setFilters],
+    [setConditions],
   );
 
   const onUpdate = useCallback(
     (oldFilter: T, updatedFilter: T) => {
-      setFilters(state =>
+      setConditions(state =>
         state.map(f => (f === oldFilter ? updatedFilter : f)),
       );
       setSaved(null);
     },
-    [setFilters],
+    [setConditions],
   );
 
   const onDelete = useCallback(
     (deletedFilter: T) => {
-      setFilters(state => state.filter(f => f !== deletedFilter));
+      setConditions(state => state.filter(f => f !== deletedFilter));
       setSaved(null);
     },
-    [setFilters],
+    [setConditions],
   );
 
-  const onCondOpChange = useCallback(
+  const onConditionsOpChange = useCallback(
     condOp => {
       setConditionsOp(condOp);
     },
@@ -54,14 +54,22 @@ export function useFilters<T extends RuleConditionEntity>(
 
   return useMemo(
     () => ({
-      filters,
+      conditions,
       saved,
       conditionsOp,
       onApply,
       onUpdate,
       onDelete,
-      onCondOpChange,
+      onConditionsOpChange,
     }),
-    [filters, saved, onApply, onUpdate, onDelete, onCondOpChange, conditionsOp],
+    [
+      conditions,
+      saved,
+      onApply,
+      onUpdate,
+      onDelete,
+      onConditionsOpChange,
+      conditionsOp,
+    ],
   );
 }
diff --git a/packages/desktop-client/src/style/themes/dark.ts b/packages/desktop-client/src/style/themes/dark.ts
index 8932bfbc7c9f9d68bbcc1fce12bf6e9e5f0929d0..3385e100a4223b9c8e3e50604cc56df376b4224c 100644
--- a/packages/desktop-client/src/style/themes/dark.ts
+++ b/packages/desktop-client/src/style/themes/dark.ts
@@ -192,3 +192,7 @@ export const reportsBlue = colorPalette.blue400;
 export const reportsGreen = colorPalette.green400;
 export const reportsLabel = pageText;
 export const reportsInnerLabel = colorPalette.navy800;
+
+export const noteTagBackground = colorPalette.purple700;
+export const noteTagBackgroundHover = colorPalette.purple500;
+export const noteTagText = colorPalette.purple100;
diff --git a/packages/desktop-client/src/style/themes/development.ts b/packages/desktop-client/src/style/themes/development.ts
index d369f5037a90f10a902bbb26f1ee8cc3b653cd60..3d73f670b6407567bce82ae2f95e47a81ab4f0d7 100644
--- a/packages/desktop-client/src/style/themes/development.ts
+++ b/packages/desktop-client/src/style/themes/development.ts
@@ -191,3 +191,7 @@ export const reportsBlue = colorPalette.blue400;
 export const reportsGreen = colorPalette.green400;
 export const reportsLabel = colorPalette.navy900;
 export const reportsInnerLabel = colorPalette.navy800;
+
+export const noteTagBackground = colorPalette.purple100;
+export const noteTagBackgroundHover = colorPalette.purple150;
+export const noteTagText = colorPalette.purple700;
diff --git a/packages/desktop-client/src/style/themes/light.ts b/packages/desktop-client/src/style/themes/light.ts
index dd3812f881b7dc858b5a888c8f7c3630c752c7fb..ad8318f869e0445a2ef2e64499c342206fee7a14 100644
--- a/packages/desktop-client/src/style/themes/light.ts
+++ b/packages/desktop-client/src/style/themes/light.ts
@@ -194,3 +194,7 @@ export const reportsBlue = colorPalette.blue400;
 export const reportsGreen = colorPalette.green400;
 export const reportsLabel = colorPalette.navy900;
 export const reportsInnerLabel = colorPalette.navy800;
+
+export const noteTagBackground = colorPalette.purple100;
+export const noteTagBackgroundHover = colorPalette.purple150;
+export const noteTagText = colorPalette.purple700;
diff --git a/packages/desktop-client/src/style/themes/midnight.ts b/packages/desktop-client/src/style/themes/midnight.ts
index 9116d3a4aa2f077c0cf17c0ce1b1e93da1f404f3..4ee705558857f598500956d8305fe3a106b69131 100644
--- a/packages/desktop-client/src/style/themes/midnight.ts
+++ b/packages/desktop-client/src/style/themes/midnight.ts
@@ -194,3 +194,7 @@ export const reportsBlue = colorPalette.blue400;
 export const reportsGreen = colorPalette.green400;
 export const reportsLabel = pageText;
 export const reportsInnerLabel = colorPalette.navy800;
+
+export const noteTagBackground = colorPalette.purple800;
+export const noteTagBackgroundHover = colorPalette.purple600;
+export const noteTagText = colorPalette.purple100;
diff --git a/packages/loot-core/src/platform/server/sqlite/index.d.ts b/packages/loot-core/src/platform/server/sqlite/index.d.ts
index 63a9575212a23eac36f909e8f9bcbfe302f7f84b..1c2b9fc04c9fca64999aecca1a982a48a829f1aa 100644
--- a/packages/loot-core/src/platform/server/sqlite/index.d.ts
+++ b/packages/loot-core/src/platform/server/sqlite/index.d.ts
@@ -1,10 +1,10 @@
-import { type Database } from 'better-sqlite3';
+import { type Database } from '@jlongster/sql.js';
 
-export async function init(): unknown;
+export async function init(): Promise<void>;
 
 export function _getModule(): SqlJsStatic;
 
-export function prepare(db, sql): unknown;
+export function prepare(db: Database, sql: string): string;
 
 export function runQuery(
   db: Database,
diff --git a/packages/loot-core/src/platform/server/sqlite/index.web.ts b/packages/loot-core/src/platform/server/sqlite/index.web.ts
index bfd273d5283c1dbb0ec8da06d6f43da80d98b93f..8930e7a3e4d4b43673a8269e737765e7806987a3 100644
--- a/packages/loot-core/src/platform/server/sqlite/index.web.ts
+++ b/packages/loot-core/src/platform/server/sqlite/index.web.ts
@@ -28,7 +28,7 @@ export function _getModule() {
   return SQL;
 }
 
-function verifyParamTypes(sql, arr) {
+function verifyParamTypes(sql: string, arr: (string | number)[] = []) {
   arr.forEach(val => {
     if (typeof val !== 'string' && typeof val !== 'number' && val !== null) {
       throw new Error('Invalid field type ' + val + ' for sql ' + sql);
@@ -36,23 +36,28 @@ function verifyParamTypes(sql, arr) {
   });
 }
 
-export function prepare(db, sql) {
+export function prepare(db: Database, sql: string) {
   return db.prepare(sql);
 }
 
 export function runQuery(
-  db: unknown,
+  db: Database,
   sql: string,
   params?: (string | number)[],
   fetchAll?: false,
 ): { changes: unknown };
 export function runQuery(
-  db: unknown,
+  db: Database,
   sql: string,
   params: (string | number)[],
   fetchAll: true,
 ): unknown[];
-export function runQuery(db, sql, params = [], fetchAll = false) {
+export function runQuery(
+  db: Database,
+  sql: string,
+  params: (string | number)[] = [],
+  fetchAll = false,
+): unknown[] | { changes: unknown } {
   if (params) {
     verifyParamTypes(sql, params);
   }
@@ -89,13 +94,13 @@ export function runQuery(db, sql, params = [], fetchAll = false) {
   }
 }
 
-export function execQuery(db, sql) {
+export function execQuery(db: Database, sql: string) {
   db.exec(sql);
 }
 
 let transactionDepth = 0;
 
-export function transaction(db, fn) {
+export function transaction(db: Database, fn: () => void) {
   let before, after, undo;
   if (transactionDepth > 0) {
     before = 'SAVEPOINT __actual_sp';
@@ -111,9 +116,8 @@ export function transaction(db, fn) {
   transactionDepth++;
 
   try {
-    const result = fn();
+    fn();
     execQuery(db, after);
-    return result;
   } catch (ex) {
     execQuery(db, undo);
 
@@ -151,6 +155,10 @@ export async function asyncTransaction(db: Database, fn: () => Promise<void>) {
   }
 }
 
+function regexp(regex: string, text: string) {
+  return new RegExp(regex).test(text) ? 1 : 0;
+}
+
 export async function openDatabase(pathOrBuffer?: string | Buffer) {
   let db = null;
   if (pathOrBuffer) {
@@ -193,6 +201,7 @@ export async function openDatabase(pathOrBuffer?: string | Buffer) {
   // but SQL.js does not support this: https://github.com/sql-js/sql.js/issues/551
   db.create_function('UNICODE_LOWER', arg => arg?.toLowerCase());
   db.create_function('UNICODE_UPPER', arg => arg?.toUpperCase());
+  db.create_function('REGEXP', regexp);
   return db;
 }
 
diff --git a/packages/loot-core/src/server/accounts/rules.ts b/packages/loot-core/src/server/accounts/rules.ts
index d164c53091fb04a8290d4ccf57c89897c75b3865..4116cd01048a3fe2c3d5000c5b702672d1bd067e 100644
--- a/packages/loot-core/src/server/accounts/rules.ts
+++ b/packages/loot-core/src/server/accounts/rules.ts
@@ -123,7 +123,15 @@ const CONDITION_TYPES = {
     },
   },
   id: {
-    ops: ['is', 'contains', 'oneOf', 'isNot', 'doesNotContain', 'notOneOf'],
+    ops: [
+      'is',
+      'contains',
+      'matches',
+      'oneOf',
+      'isNot',
+      'doesNotContain',
+      'notOneOf',
+    ],
     nullable: true,
     parse(op, value, fieldName) {
       if (op === 'oneOf' || op === 'notOneOf') {
@@ -138,7 +146,15 @@ const CONDITION_TYPES = {
     },
   },
   string: {
-    ops: ['is', 'contains', 'oneOf', 'isNot', 'doesNotContain', 'notOneOf'],
+    ops: [
+      'is',
+      'contains',
+      'matches',
+      'oneOf',
+      'isNot',
+      'doesNotContain',
+      'notOneOf',
+    ],
     nullable: true,
     parse(op, value, fieldName) {
       if (op === 'oneOf' || op === 'notOneOf') {
@@ -152,7 +168,7 @@ const CONDITION_TYPES = {
         return value.filter(Boolean).map(val => val.toLowerCase());
       }
 
-      if (op === 'contains' || op === 'doesNotContain') {
+      if (op === 'contains' || op === 'matches' || op === 'doesNotContain') {
         assert(
           typeof value === 'string' && value.length > 0,
           'no-empty-string',
@@ -812,6 +828,7 @@ const OP_SCORES: Record<RuleConditionEntity['op'], number> = {
   lte: 1,
   contains: 0,
   doesNotContain: 0,
+  matches: 0,
 };
 
 function computeScore(rule) {
diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/accounts/transaction-rules.ts
index 3119cdd69c0fb662b1a160f528a3dc08da9ee29c..be667ce6533853729091026874d40ff60fa85514 100644
--- a/packages/loot-core/src/server/accounts/transaction-rules.ts
+++ b/packages/loot-core/src/server/accounts/transaction-rules.ts
@@ -452,6 +452,10 @@ export function conditionsToAQL(conditions, { recurDateBounds = 100 } = {}) {
           '$like',
           '%' + value + '%',
         );
+      case 'matches':
+        // Running contains with id will automatically reach into
+        // the `name` of the referenced table and do a regex match
+        return apply(type === 'id' ? field + '.name' : field, '$regexp', value);
       case 'doesNotContain':
         // Running contains with id will automatically reach into
         // the `name` of the referenced table and do a string match
diff --git a/packages/loot-core/src/server/aql/compiler.ts b/packages/loot-core/src/server/aql/compiler.ts
index 66c4e372373fed1b3d2647f327756b44273cd92e..5488efa3ed1764412cebb122f73a400d9a344e0d 100644
--- a/packages/loot-core/src/server/aql/compiler.ts
+++ b/packages/loot-core/src/server/aql/compiler.ts
@@ -722,6 +722,10 @@ const compileOp = saveStack('op', (state, fieldRef, opData) => {
       const [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);
       return `${left} LIKE ${right}`;
     }
+    case '$regexp': {
+      const [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);
+      return `REGEXP(${right}, ${left})`;
+    }
     case '$notlike': {
       const [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);
       return `(${left} NOT LIKE ${right}\n OR ${left} IS NULL)`;
diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts
index 507401875c96bc8b2295834cbf097b40294b357e..a578b408c0dda9a0ff4854559da3d402846125c0 100644
--- a/packages/loot-core/src/server/db/index.ts
+++ b/packages/loot-core/src/server/db/index.ts
@@ -7,6 +7,7 @@ import {
   makeClientId,
   Timestamp,
 } from '@actual-app/crdt';
+import { Database } from '@jlongster/sql.js';
 import LRU from 'lru-cache';
 import { v4 as uuidv4 } from 'uuid';
 
@@ -37,8 +38,8 @@ import { shoveSortOrders, SORT_INCREMENT } from './sort';
 
 export { toDateRepr, fromDateRepr } from '../models';
 
-let dbPath;
-let db;
+let dbPath: string | null = null;
+let db: Database | null = null;
 
 // Util
 
@@ -46,7 +47,7 @@ export function getDatabasePath() {
   return dbPath;
 }
 
-export async function openDatabase(id?) {
+export async function openDatabase(id?: string) {
   if (db) {
     await sqlite.closeDatabase(db);
   }
@@ -57,11 +58,6 @@ export async function openDatabase(id?) {
   // await execQuery('PRAGMA journal_mode = WAL');
 }
 
-export async function reopenDatabase() {
-  await sqlite.closeDatabase(db);
-  setDatabase(await sqlite.openDatabase(dbPath));
-}
-
 export async function closeDatabase() {
   if (db) {
     await sqlite.closeDatabase(db);
@@ -69,7 +65,7 @@ export async function closeDatabase() {
   }
 }
 
-export function setDatabase(db_) {
+export function setDatabase(db_: Database) {
   db = db_;
   resetQueryCache();
 }
@@ -115,14 +111,14 @@ export function runQuery(sql, params, fetchAll) {
   return result;
 }
 
-export function execQuery(sql) {
+export function execQuery(sql: string) {
   sqlite.execQuery(db, sql);
 }
 
 // This manages an LRU cache of prepared query statements. This is
 // only needed in hot spots when you are running lots of queries.
 let _queryCache = new LRU({ max: 100 });
-export function cache(sql) {
+export function cache(sql: string) {
   const cached = _queryCache.get(sql);
   if (cached) {
     return cached;
diff --git a/packages/loot-core/src/server/migrate/migrations.ts b/packages/loot-core/src/server/migrate/migrations.ts
index a34960d68e27f0d49e14cafe6defd6484b289e4d..0726ec893a0619dcd559e2402dd051b1c8e0be9f 100644
--- a/packages/loot-core/src/server/migrate/migrations.ts
+++ b/packages/loot-core/src/server/migrate/migrations.ts
@@ -2,7 +2,7 @@
 // We have to bundle in JS migrations manually to avoid having to `eval`
 // them which doesn't play well with CSP. There isn't great, and eventually
 // we can remove this migration.
-import { Database } from 'better-sqlite3';
+import { Database } from '@jlongster/sql.js';
 import { v4 as uuidv4 } from 'uuid';
 
 import m1632571489012 from '../../../migrations/1632571489012_remove_cache';
diff --git a/packages/loot-core/src/server/sheet.ts b/packages/loot-core/src/server/sheet.ts
index 65ce0e34ca55ac792b6acb1c9ad1650c921ee64b..3ac4b6e7b197cccba40493ae17679379499f9b4e 100644
--- a/packages/loot-core/src/server/sheet.ts
+++ b/packages/loot-core/src/server/sheet.ts
@@ -1,5 +1,5 @@
 // @ts-strict-ignore
-import { type Database } from 'better-sqlite3';
+import { type Database } from '@jlongster/sql.js';
 
 import { captureBreadcrumb } from '../platform/exceptions';
 import * as sqlite from '../platform/server/sqlite';
diff --git a/packages/loot-core/src/shared/rules.ts b/packages/loot-core/src/shared/rules.ts
index c54b8723a3a6ea09963a27aa14e4615d24d93ba8..a2c53e8a4b258038805aeca8f31ceb337de2c0db 100644
--- a/packages/loot-core/src/shared/rules.ts
+++ b/packages/loot-core/src/shared/rules.ts
@@ -9,7 +9,15 @@ export const TYPE_INFO = {
     nullable: false,
   },
   id: {
-    ops: ['is', 'contains', 'oneOf', 'isNot', 'doesNotContain', 'notOneOf'],
+    ops: [
+      'is',
+      'contains',
+      'matches',
+      'oneOf',
+      'isNot',
+      'doesNotContain',
+      'notOneOf',
+    ],
     nullable: true,
   },
   saved: {
@@ -17,7 +25,15 @@ export const TYPE_INFO = {
     nullable: false,
   },
   string: {
-    ops: ['is', 'contains', 'oneOf', 'isNot', 'doesNotContain', 'notOneOf'],
+    ops: [
+      'is',
+      'contains',
+      'matches',
+      'oneOf',
+      'isNot',
+      'doesNotContain',
+      'notOneOf',
+    ],
     nullable: true,
   },
   number: {
@@ -91,6 +107,8 @@ export function friendlyOp(op, type?) {
       return 'is between';
     case 'contains':
       return 'contains';
+    case 'matches':
+      return 'matches';
     case 'doesNotContain':
       return 'does not contain';
     case 'gt':
diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts
index 026aef47a259e06a33321af21fefbcbfdd42ff01..793d36dc2561151e06c502c0232ee857a6aecb4b 100644
--- a/packages/loot-core/src/types/models/rule.d.ts
+++ b/packages/loot-core/src/types/models/rule.d.ts
@@ -24,7 +24,8 @@ export type RuleConditionOp =
   | 'lt'
   | 'lte'
   | 'contains'
-  | 'doesNotContain';
+  | 'doesNotContain'
+  | 'matches';
 
 export interface RuleConditionEntity {
   field?: string;
diff --git a/upcoming-release-notes/2670.md b/upcoming-release-notes/2670.md
new file mode 100644
index 0000000000000000000000000000000000000000..0fd902c06af795933be782ec3043489f4c85848f
--- /dev/null
+++ b/upcoming-release-notes/2670.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [joel-jeremy]
+---
+
+Format notes that starts with # as clickable tags.