diff --git a/packages/desktop-client/src/components/ManageRules.js b/packages/desktop-client/src/components/ManageRules.js
index 8f3715754108edc4fb0513fa67acfd9deaf71b06..5a26a7bd5df88dae6b4d7924b8e5c38cbd1c4378 100644
--- a/packages/desktop-client/src/components/ManageRules.js
+++ b/packages/desktop-client/src/components/ManageRules.js
@@ -208,7 +208,15 @@ export function Value({
   }
 }
 
-function ConditionExpression({ field, op, value, options, prefix, style }) {
+function ConditionExpression({
+  field,
+  op,
+  value,
+  options,
+  prefix,
+  style,
+  inline,
+}) {
   return (
     <View
       style={[
@@ -228,7 +236,7 @@ function ConditionExpression({ field, op, value, options, prefix, style }) {
       {prefix && <Text style={{ color: colors.n3 }}>{prefix} </Text>}
       <Text style={{ color: colors.p4 }}>{mapField(field, options)}</Text>{' '}
       <Text style={{ color: colors.n3 }}>{friendlyOp(op)}</Text>{' '}
-      <Value value={value} field={field} />
+      <Value value={value} field={field} inline={inline} />
     </View>
   );
 }
@@ -355,6 +363,7 @@ let Rule = memo(
                   key={i}
                   field={cond.field}
                   op={cond.op}
+                  inline={true}
                   value={cond.value}
                   options={cond.options}
                   prefix={i > 0 ? friendlyOp(rule.conditionsOp) : null}
diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js
index 9f1c91b53395f00a0199f309bf26e860f43e140c..b61099ee2063a57f28a6c32f34208f1b21158340 100644
--- a/packages/desktop-client/src/components/accounts/Account.js
+++ b/packages/desktop-client/src/components/accounts/Account.js
@@ -19,6 +19,7 @@ import { debounce } from 'debounce';
 import { bindActionCreators } from 'redux';
 
 import * as actions from 'loot-core/src/client/actions';
+import { useFilters } from 'loot-core/src/client/data-hooks/filters';
 import {
   SchedulesProvider,
   useCachedSchedules,
@@ -71,6 +72,8 @@ import {
   Menu,
   Stack,
 } from '../common';
+import { FilterButton } from '../filters/FiltersMenu';
+import { FiltersStack } from '../filters/SavedFilters';
 import { KeyHandlers } from '../KeyHandlers';
 import NotesButton from '../NotesButton';
 import CellValue from '../spreadsheet/CellValue';
@@ -78,7 +81,6 @@ import format from '../spreadsheet/format';
 import useSheetValue from '../spreadsheet/useSheetValue';
 import { SelectedItemsButton } from '../table';
 
-import { FilterButton, AppliedFilters } from './Filters';
 import TransactionList from './TransactionList';
 import {
   SplitsExpandedProvider,
@@ -253,11 +255,11 @@ function MenuButton({ onClick }) {
   );
 }
 
-function MenuTooltip({ onClose, children }) {
+export function MenuTooltip({ width, onClose, children }) {
   return (
     <Tooltip
       position="bottom-right"
-      width={200}
+      width={width}
       style={{ padding: 0 }}
       onClose={onClose}
     >
@@ -286,7 +288,7 @@ function AccountMenu({
       onReconcile={onReconcile}
     />
   ) : (
-    <MenuTooltip onClose={onClose}>
+    <MenuTooltip width={200} onClose={onClose}>
       <Menu
         onMenuSelect={item => {
           if (item === 'reconcile') {
@@ -328,7 +330,7 @@ function AccountMenu({
 
 function CategoryMenu({ onClose, onMenuSelect }) {
   return (
-    <MenuTooltip onClose={onClose}>
+    <MenuTooltip width={200} onClose={onClose}>
       <Menu
         onMenuSelect={item => {
           onMenuSelect(item);
@@ -674,6 +676,8 @@ const AccountHeader = memo(
     workingHard,
     accountName,
     account,
+    filterId,
+    filtersList,
     accountsSyncing,
     accounts,
     transactions,
@@ -686,6 +690,7 @@ const AccountHeader = memo(
     canCalculateBalance,
     search,
     filters,
+    conditionsOp,
     savePrefs,
     onSearch,
     onAddTransaction,
@@ -706,6 +711,9 @@ const AccountHeader = memo(
     onCreateRule,
     onApplyFilter,
     onUpdateFilter,
+    onClearFilters,
+    onReloadSavedFilter,
+    onCondOpChange,
     onDeleteFilter,
     onScheduleAction,
   }) => {
@@ -1010,10 +1018,16 @@ const AccountHeader = memo(
           </Stack>
 
           {filters && filters.length > 0 && (
-            <AppliedFilters
+            <FiltersStack
               filters={filters}
-              onUpdate={onUpdateFilter}
-              onDelete={onDeleteFilter}
+              conditionsOp={conditionsOp}
+              onUpdateFilter={onUpdateFilter}
+              onDeleteFilter={onDeleteFilter}
+              onClearFilters={onClearFilters}
+              onReloadSavedFilter={onReloadSavedFilter}
+              filterId={filterId}
+              filtersList={filtersList}
+              onCondOpChange={onCondOpChange}
             />
           )}
         </View>
@@ -1096,6 +1110,8 @@ class AccountInternal extends PureComponent {
       editingName: false,
       isAdding: false,
       latestDate: null,
+      filterId: [],
+      conditionsOp: 'and',
     };
   }
 
@@ -1760,14 +1776,56 @@ class AccountInternal extends PureComponent {
     this.props.pushModal('edit-rule', { rule });
   };
 
+  onCondOpChange = (value, filters) => {
+    this.setState({ conditionsOp: value });
+    this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
+    this.applyFilters([...filters]);
+  };
+
+  onReloadSavedFilter = (savedFilter, item) => {
+    if (item === 'reload') {
+      let [getFilter] = this.props.filtersList.filter(
+        f => f.id === this.state.filterId.id,
+      );
+      this.setState({ conditionsOp: getFilter.conditionsOp });
+      this.applyFilters([...getFilter.conditions]);
+    } else {
+      savedFilter.status &&
+        this.setState({ conditionsOp: savedFilter.conditionsOp }) &&
+        this.applyFilters([...savedFilter.conditions]);
+    }
+    this.setState({ filterId: { ...this.state.filterId, ...savedFilter } });
+  };
+
+  onClearFilters = () => {
+    this.setState({ conditionsOp: 'and' });
+    this.setState({ filterId: [] });
+    this.applyFilters([]);
+  };
+
   onUpdateFilter = (oldFilter, updatedFilter) => {
     this.applyFilters(
       this.state.filters.map(f => (f === oldFilter ? updatedFilter : f)),
     );
+    this.setState({
+      filterId: {
+        ...this.state.filterId,
+        status: this.state.filterId && 'changed',
+      },
+    });
   };
 
   onDeleteFilter = filter => {
     this.applyFilters(this.state.filters.filter(f => f !== filter));
+    this.state.filters.length === 1
+      ? this.setState({ filterId: [] }) &&
+        this.setState({ conditionsOp: 'and' })
+      : this.setState({
+          filterId: {
+            ...this.state.filterId,
+            status: this.state.filterId && 'changed',
+          },
+        });
   };
 
   onApplyFilter = async cond => {
@@ -1775,7 +1833,19 @@ class AccountInternal extends PureComponent {
     if (cond.customName) {
       filters = filters.filter(f => f.customName !== cond.customName);
     }
-    this.applyFilters([...filters, cond]);
+    if (cond.conditions) {
+      this.setState({ filterId: { ...cond, status: 'saved' } });
+      this.setState({ conditionsOp: cond.conditionsOp });
+      this.applyFilters([...cond.conditions]);
+    } else {
+      this.setState({
+        filterId: {
+          ...this.state.filterId,
+          status: this.state.filterId && 'changed',
+        },
+      });
+      this.applyFilters([...filters, cond]);
+    }
   };
 
   onScheduleAction = async (name, ids) => {
@@ -1805,9 +1875,9 @@ class AccountInternal extends PureComponent {
       let { filters } = await send('make-filters-from-conditions', {
         conditions: conditions.filter(cond => !cond.customName),
       });
-
+      const conditionsOpKey = this.state.conditionsOp === 'or' ? '$or' : '$and';
       this.currentQuery = this.rootQuery.filter({
-        $and: [...filters, ...customFilters],
+        [conditionsOpKey]: [...filters, ...customFilters],
       });
       this.updateQuery(this.currentQuery, true);
       this.setState({ filters: conditions, search: '' });
@@ -1836,6 +1906,7 @@ class AccountInternal extends PureComponent {
       transactions,
       loading,
       workingHard,
+      filterId,
       reconcileAmount,
       transactionsFiltered,
       editingName,
@@ -1881,13 +1952,16 @@ class AccountInternal extends PureComponent {
               fetchAllIds={this.fetchAllIds}
               registerDispatch={dispatch => (this.dispatchSelected = dispatch)}
             >
-              <View style={[styles.page, { backgroundColor: colors.n11 }]}>
+              <View style={[styles.page]}>
                 <AccountHeader
                   tableRef={this.table}
                   editingName={editingName}
                   isNameEditable={isNameEditable}
                   workingHard={workingHard}
                   account={account}
+                  filterId={filterId}
+                  filtersList={this.props.filtersList}
+                  location={this.props.location}
                   accountName={accountName}
                   accountsSyncing={accountsSyncing}
                   accounts={accounts}
@@ -1901,6 +1975,7 @@ class AccountInternal extends PureComponent {
                   reconcileAmount={reconcileAmount}
                   search={this.state.search}
                   filters={this.state.filters}
+                  conditionsOp={this.state.conditionsOp}
                   savePrefs={this.props.savePrefs}
                   onSearch={this.onSearch}
                   onShowTransactions={this.onShowTransactions}
@@ -1922,6 +1997,9 @@ class AccountInternal extends PureComponent {
                   onBatchUnlink={this.onBatchUnlink}
                   onCreateRule={this.onCreateRule}
                   onUpdateFilter={this.onUpdateFilter}
+                  onClearFilters={this.onClearFilters}
+                  onReloadSavedFilter={this.onReloadSavedFilter}
+                  onCondOpChange={this.onCondOpChange}
                   onDeleteFilter={this.onDeleteFilter}
                   onApplyFilter={this.onApplyFilter}
                   onScheduleAction={this.onScheduleAction}
@@ -2040,6 +2118,7 @@ export default function Account() {
   }));
 
   let dispatch = useDispatch();
+  let filtersList = useFilters();
   let actionCreators = useMemo(
     () => bindActionCreators(actions, dispatch),
     [dispatch],
@@ -2085,6 +2164,7 @@ export default function Account() {
           accountId={params.id}
           categoryId={activeLocation?.state?.filter?.category}
           location={location}
+          filtersList={filtersList}
         />
       </SplitsExpandedProvider>
     </SchedulesProvider>
diff --git a/packages/desktop-client/src/components/autocomplete/SavedFilterAutocomplete.js b/packages/desktop-client/src/components/autocomplete/SavedFilterAutocomplete.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b944a9efc073201cbf95fb87a697642ab9a41c1
--- /dev/null
+++ b/packages/desktop-client/src/components/autocomplete/SavedFilterAutocomplete.js
@@ -0,0 +1,64 @@
+import React from 'react';
+
+import { useFilters } from 'loot-core/src/client/data-hooks/filters';
+
+import { colors } from '../../style';
+import { View } from '../common';
+
+import Autocomplete from './Autocomplete';
+
+function FilterList({ items, getItemProps, highlightedIndex, embedded }) {
+  return (
+    <View>
+      <View
+        style={[
+          { overflow: 'auto', padding: '5px 0' },
+          !embedded && { maxHeight: 175 },
+        ]}
+      >
+        {items.map((item, idx) => {
+          return [
+            <div
+              {...(getItemProps ? getItemProps({ item }) : null)}
+              key={item.id}
+              style={{
+                backgroundColor:
+                  highlightedIndex === idx ? colors.n4 : 'transparent',
+                padding: 4,
+                paddingLeft: 20,
+                borderRadius: embedded ? 4 : 0,
+              }}
+              data-testid={
+                'filter-item' + (highlightedIndex === idx ? '-highlighted' : '')
+              }
+            >
+              {item.name}
+            </div>,
+          ];
+        })}
+      </View>
+    </View>
+  );
+}
+
+export default function SavedFilterAutocomplete({ embedded, ...props }) {
+  let filters = useFilters() || [];
+
+  return (
+    <Autocomplete
+      strict={true}
+      highlightFirst={true}
+      embedded={embedded}
+      suggestions={filters}
+      renderItems={(items, getItemProps, highlightedIndex) => (
+        <FilterList
+          items={items}
+          getItemProps={getItemProps}
+          highlightedIndex={highlightedIndex}
+          embedded={embedded}
+        />
+      )}
+      {...props}
+    />
+  );
+}
diff --git a/packages/desktop-client/src/components/common.tsx b/packages/desktop-client/src/components/common.tsx
index 63af5c81be83a36d4feca963ae05a71dbb4b99e8..9e53e0b3634e207b4604f4e44518c0d96e89b485 100644
--- a/packages/desktop-client/src/components/common.tsx
+++ b/packages/desktop-client/src/components/common.tsx
@@ -519,6 +519,7 @@ export function Menu({
                 lineHeight: '1em',
                 textTransform: 'uppercase',
                 margin: '3px 9px',
+                marginTop: 5,
               }}
             >
               {item.name}
diff --git a/packages/desktop-client/src/components/accounts/Filters.js b/packages/desktop-client/src/components/filters/FiltersMenu.js
similarity index 92%
rename from packages/desktop-client/src/components/accounts/Filters.js
rename to packages/desktop-client/src/components/filters/FiltersMenu.js
index b0a552258c5cb2f82ed591f23fd2d104b826e57a..b63b2d6cc95d62a469bb981c7c5ec56ca0844b9e 100644
--- a/packages/desktop-client/src/components/accounts/Filters.js
+++ b/packages/desktop-client/src/components/filters/FiltersMenu.js
@@ -8,6 +8,7 @@ import {
   isValid as isDateValid,
 } from 'date-fns';
 
+import { useFilters } from 'loot-core/src/client/data-hooks/filters';
 import { send } from 'loot-core/src/platform/client/fetch';
 import { getMonthYearFormat } from 'loot-core/src/shared/months';
 import {
@@ -37,6 +38,8 @@ import {
 import { Value } from '../ManageRules';
 import GenericInput from '../util/GenericInput';
 
+import { CondOpMenu } from './SavedFilters';
+
 let filterFields = [
   'date',
   'account',
@@ -45,6 +48,7 @@ let filterFields = [
   'category',
   'amount',
   'cleared',
+  'saved',
 ].map(field => [field, mapField(field)]);
 
 function subfieldFromFilter({ field, options, value }) {
@@ -163,7 +167,7 @@ function ConfigureField({
     <Tooltip
       position="bottom-left"
       style={{ padding: 15 }}
-      width={300}
+      width={250}
       onClose={() => dispatch({ type: 'close' })}
     >
       <FocusScope>
@@ -200,6 +204,15 @@ function ConfigureField({
           )}
         </View>
 
+        <View
+          style={{
+            color: colors.n4,
+            marginBottom: 10,
+          }}
+        >
+          {field === 'saved' && 'Existing filters will be cleared'}
+        </View>
+
         <Stack
           direction="row"
           align="flex-start"
@@ -251,7 +264,8 @@ function ConfigureField({
             />
           )}
 
-          <View>
+          <Stack direction="row" justify="flex-end" align="center">
+            <View style={{ flex: 1 }} />
             <Button
               primary
               style={{ marginTop: 15 }}
@@ -267,7 +281,7 @@ function ConfigureField({
             >
               Apply
             </Button>
-          </View>
+          </Stack>
         </form>
       </FocusScope>
     </Tooltip>
@@ -275,6 +289,8 @@ function ConfigureField({
 }
 
 export function FilterButton({ onApply }) {
+  let filters = useFilters();
+
   let { dateFormat } = useSelector(state => {
     return {
       dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
@@ -335,16 +351,20 @@ export function FilterButton({ onApply }) {
       }
     }
 
-    let { error } = await send('rule-validate', {
-      conditions: [cond],
-      actions: [],
-    });
+    let { error } =
+      cond.field !== 'saved' &&
+      (await send('rule-validate', {
+        conditions: [cond],
+        actions: [],
+      }));
+
+    let saved = filters.find(f => cond.value === f.id);
 
     if (error && error.conditionErrors.length > 0) {
       let field = titleFirst(mapField(cond.field));
       alert(field + ': ' + getFieldError(error.conditionErrors[0]));
     } else {
-      onApply(cond);
+      onApply(saved ? saved : cond);
       dispatch({ type: 'close' });
     }
   }
@@ -441,12 +461,12 @@ function FilterExpression({
     <View
       style={[
         {
-          backgroundColor: colors.n10,
+          backgroundColor: colors.n9,
           borderRadius: 4,
           flexDirection: 'row',
           alignItems: 'center',
-          marginBottom: 10,
           marginRight: 10,
+          marginTop: 10,
         },
         style,
       ]}
@@ -503,17 +523,27 @@ function FilterExpression({
   );
 }
 
-export function AppliedFilters({ filters, editingFilter, onUpdate, onDelete }) {
+export function AppliedFilters({
+  filters,
+  editingFilter,
+  onUpdate,
+  onDelete,
+  conditionsOp,
+  onCondOpChange,
+}) {
   return (
     <View
       style={{
         flexDirection: 'row',
-        alignItems: 'center',
+        alignItems: 'flex-start',
         flexWrap: 'wrap',
-        marginTop: 10,
-        marginBottom: -5,
       }}
     >
+      <CondOpMenu
+        conditionsOp={conditionsOp}
+        onCondOpChange={onCondOpChange}
+        filters={filters}
+      />
       {filters.map((filter, i) => (
         <FilterExpression
           key={i}
diff --git a/packages/desktop-client/src/components/filters/SavedFilters.js b/packages/desktop-client/src/components/filters/SavedFilters.js
new file mode 100644
index 0000000000000000000000000000000000000000..a243cf8f4b3f6bf2c305b8de0701f3a920a5208c
--- /dev/null
+++ b/packages/desktop-client/src/components/filters/SavedFilters.js
@@ -0,0 +1,318 @@
+import React, { useState, useRef, useEffect } from 'react';
+
+import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
+
+import ExpandArrow from '../../icons/v0/ExpandArrow';
+import { colors } from '../../style';
+import { MenuTooltip } from '../accounts/Account';
+import { View, Text, Button, Menu, Stack } from '../common';
+import { FormField, FormLabel } from '../forms';
+import { FieldSelect } from '../modals/EditRule';
+import GenericInput from '../util/GenericInput';
+
+import { AppliedFilters } from './FiltersMenu';
+
+function SavedFilterMenuButton({
+  filters,
+  conditionsOp,
+  filterId,
+  onClearFilters,
+  onReloadSavedFilter,
+  filtersList,
+}) {
+  let [nameOpen, setNameOpen] = useState(false);
+  let [adding, setAdding] = useState(false);
+  let [menuOpen, setMenuOpen] = useState(false);
+  let [err, setErr] = useState(null);
+  let [menuItem, setMenuItem] = useState(null);
+  let inputRef = useRef();
+  let name = filterId.name;
+  let id = filterId.id;
+  let res;
+  let savedFilter;
+
+  useEffect(() => {
+    if (inputRef.current) {
+      inputRef.current.focus();
+    }
+  }, [NameFilter]);
+
+  const onFilterMenuSelect = async item => {
+    setMenuItem(item);
+    switch (item) {
+      case 'rename-filter':
+        setErr(null);
+        setAdding(false);
+        setMenuOpen(false);
+        setNameOpen(true);
+        break;
+      case 'delete-filter':
+        setMenuOpen(false);
+        await send('filter-delete', id);
+        onClearFilters();
+        break;
+      case 'update-filter':
+        setErr(null);
+        setAdding(false);
+        setMenuOpen(false);
+        savedFilter = {
+          conditions: filters,
+          conditionsOp: conditionsOp,
+          id: filterId.id,
+          name: filterId.name,
+          status: 'saved',
+        };
+        res = await sendCatch('filter-update', {
+          state: savedFilter,
+          filters: [...filtersList],
+        });
+        if (res.error) {
+          setErr(res.error.message);
+          setNameOpen(true);
+        } else {
+          onReloadSavedFilter(savedFilter, 'update');
+        }
+        break;
+      case 'save-filter':
+        setErr(null);
+        setAdding(true);
+        setMenuOpen(false);
+        setNameOpen(true);
+        break;
+      case 'reload-filter':
+        setMenuOpen(false);
+        savedFilter = {
+          status: 'saved',
+        };
+        onReloadSavedFilter(savedFilter, 'reload');
+        break;
+      case 'clear-filter':
+        setMenuOpen(false);
+        onClearFilters();
+        break;
+      default:
+    }
+  };
+
+  function FilterMenu({ onClose, filterId }) {
+    return (
+      <MenuTooltip width={200} onClose={onClose}>
+        <Menu
+          onMenuSelect={item => {
+            onFilterMenuSelect(item);
+          }}
+          items={[
+            ...(!filterId.id
+              ? [
+                  { name: 'save-filter', text: 'Save new filter' },
+                  { name: 'clear-filter', text: 'Clear all conditions' },
+                ]
+              : [
+                  ...(filterId.id !== null && filterId.status === 'saved'
+                    ? [
+                        { name: 'rename-filter', text: 'Rename' },
+                        { name: 'delete-filter', text: 'Delete' },
+                        Menu.line,
+                        {
+                          name: 'save-filter',
+                          text: 'Save new filter',
+                          disabled: true,
+                        },
+                        { name: 'clear-filter', text: 'Clear all conditions' },
+                      ]
+                    : [
+                        { name: 'rename-filter', text: 'Rename' },
+                        { name: 'update-filter', text: 'Update condtions' },
+                        { name: 'reload-filter', text: 'Revert changes' },
+                        { name: 'delete-filter', text: 'Delete' },
+                        Menu.line,
+                        { name: 'save-filter', text: 'Save new filter' },
+                        { name: 'clear-filter', text: 'Clear all conditions' },
+                      ]),
+                ]),
+          ]}
+        />
+      </MenuTooltip>
+    );
+  }
+
+  async function onAddUpdate() {
+    if (adding) {
+      //create new flow
+      savedFilter = {
+        conditions: filters,
+        conditionsOp: conditionsOp,
+        name: name,
+        status: 'saved',
+      };
+      res = await sendCatch('filter-create', {
+        state: savedFilter,
+        filters: [...filtersList],
+      });
+      savedFilter = {
+        ...savedFilter,
+        id: res.data,
+      };
+    } else {
+      //rename flow
+      savedFilter = {
+        conditions: filterId.conditions,
+        conditionsOp: filterId.conditionsOp,
+        id: filterId.id,
+        name: name,
+      };
+      res = await sendCatch('filter-update', {
+        state: savedFilter,
+        filters: [...filtersList],
+      });
+    }
+    if (res.error) {
+      setErr(res.error.message);
+    } else {
+      setNameOpen(false);
+      onReloadSavedFilter(savedFilter);
+    }
+  }
+
+  function NameFilter({ onClose }) {
+    return (
+      <MenuTooltip width={325} onClose={onClose}>
+        {menuItem !== 'update-filter' && (
+          <form>
+            <Stack
+              direction="row"
+              justify="flex-end"
+              align="center"
+              style={{ padding: 10 }}
+            >
+              <FormField style={{ flex: 1 }}>
+                <FormLabel
+                  title="Filter Name"
+                  htmlFor="name-field"
+                  style={{ userSelect: 'none' }}
+                />
+                <GenericInput
+                  inputRef={inputRef}
+                  id="name-field"
+                  field="string"
+                  type="string"
+                  value={name}
+                  onChange={e => (name = e)}
+                />
+              </FormField>
+              <Button
+                primary
+                type="submit"
+                style={{ marginTop: 18 }}
+                onClick={e => {
+                  e.preventDefault();
+                  onAddUpdate();
+                }}
+              >
+                {adding ? 'Add' : 'Update'}
+              </Button>
+            </Stack>
+          </form>
+        )}
+        {err && (
+          <Stack direction="row" align="center" style={{ padding: 10 }}>
+            <Text style={{ color: colors.r4 }}>{err}</Text>
+          </Stack>
+        )}
+      </MenuTooltip>
+    );
+  }
+
+  return (
+    <View>
+      {filters.length > 0 && (
+        <Button
+          bare
+          style={{ marginTop: 10 }}
+          onClick={() => {
+            setMenuOpen(true);
+          }}
+        >
+          <Text
+            style={{
+              maxWidth: 150,
+              whiteSpace: 'nowrap',
+              overflow: 'hidden',
+              textOverflow: 'ellipsis',
+              flexShrink: 0,
+            }}
+          >
+            {!filterId.id ? 'Unsaved filter' : filterId.name}&nbsp;
+          </Text>
+          {filterId.id && filterId.status !== 'saved' && (
+            <Text>(modified)&nbsp;</Text>
+          )}
+          <ExpandArrow width={8} height={8} style={{ marginRight: 5 }} />
+        </Button>
+      )}
+      {menuOpen && (
+        <FilterMenu onClose={() => setMenuOpen(false)} filterId={filterId} />
+      )}
+      {nameOpen && <NameFilter onClose={() => setNameOpen(false)} />}
+    </View>
+  );
+}
+
+export function CondOpMenu({ conditionsOp, onCondOpChange, filters }) {
+  return (
+    filters.length > 1 && (
+      <Text style={{ color: colors.n4, marginTop: 11, marginRight: 5 }}>
+        <FieldSelect
+          style={{ display: 'inline-flex' }}
+          fields={[
+            ['and', 'all'],
+            ['or', 'any'],
+          ]}
+          value={conditionsOp}
+          onChange={(name, value) => onCondOpChange(value, filters)}
+        />
+        of:
+      </Text>
+    )
+  );
+}
+
+export function FiltersStack({
+  filters,
+  conditionsOp,
+  onUpdateFilter,
+  onDeleteFilter,
+  onClearFilters,
+  onReloadSavedFilter,
+  filterId,
+  filtersList,
+  onCondOpChange,
+}) {
+  return (
+    <View>
+      <Stack
+        spacing={2}
+        direction="row"
+        justify="flex-start"
+        align="flex-start"
+      >
+        <AppliedFilters
+          filters={filters}
+          conditionsOp={conditionsOp}
+          onCondOpChange={onCondOpChange}
+          onUpdate={onUpdateFilter}
+          onDelete={onDeleteFilter}
+        />
+        <View style={{ flex: 1 }} />
+        <SavedFilterMenuButton
+          filters={filters}
+          conditionsOp={conditionsOp}
+          filterId={filterId}
+          onClearFilters={onClearFilters}
+          onReloadSavedFilter={onReloadSavedFilter}
+          filtersList={filtersList}
+        />
+      </Stack>
+    </View>
+  );
+}
diff --git a/packages/desktop-client/src/components/modals/EditRule.js b/packages/desktop-client/src/components/modals/EditRule.js
index f0ce39494b8819193dd667eaf2bc458bbf7f8d77..bfb2594218b76a0ef07ee34a55cb4a9b1910dce7 100644
--- a/packages/desktop-client/src/components/modals/EditRule.js
+++ b/packages/desktop-client/src/components/modals/EditRule.js
@@ -78,7 +78,7 @@ function getTransactionFields(conditions, actions) {
   return fields;
 }
 
-function FieldSelect({ fields, style, value, onChange }) {
+export function FieldSelect({ fields, style, value, onChange }) {
   return (
     <View style={style}>
       <CustomSelect
diff --git a/packages/desktop-client/src/components/reports/CashFlow.js b/packages/desktop-client/src/components/reports/CashFlow.js
index c33a373172a1cee1ef14b6a95b4755429a62c692..5c5f652c545a3e3585123e0dd3b1bbbe9f8f4a1a 100644
--- a/packages/desktop-client/src/components/reports/CashFlow.js
+++ b/packages/desktop-client/src/components/reports/CashFlow.js
@@ -8,7 +8,6 @@ import { integerToCurrency } from 'loot-core/src/shared/util';
 
 import useFilters from '../../hooks/useFilters';
 import { colors, styles } from '../../style';
-import { FilterButton, AppliedFilters } from '../accounts/Filters';
 import { View, Text, Block, P, AlignedText } from '../common';
 
 import Change from './Change';
@@ -20,9 +19,11 @@ import useReport from './useReport';
 function CashFlow() {
   const {
     filters,
+    conditionsOp,
     onApply: onApplyFilter,
     onDelete: onDeleteFilter,
     onUpdate: onUpdateFilter,
+    onCondOpChange,
   } = useFilters();
 
   const [allMonths, setAllMonths] = useState(null);
@@ -40,8 +41,8 @@ function CashFlow() {
   });
 
   const params = useMemo(
-    () => cashFlowByDate(start, end, isConcise, filters),
-    [start, end, isConcise, filters],
+    () => cashFlowByDate(start, end, isConcise, filters, conditionsOp),
+    [start, end, isConcise, filters, conditionsOp],
   );
   const data = useReport('cash_flow', params);
 
@@ -97,31 +98,19 @@ function CashFlow() {
         end={monthUtils.getMonth(end)}
         show1Month
         onChangeDates={onChangeDates}
-        extraButtons={<FilterButton onApply={onApplyFilter} />}
+        onApply={onApplyFilter}
+        filters={filters}
+        onUpdateFilter={onUpdateFilter}
+        onDeleteFilter={onDeleteFilter}
+        conditionsOp={conditionsOp}
+        onCondOpChange={onCondOpChange}
       />
 
       <View
         style={{
-          marginTop: -10,
-          paddingLeft: 20,
-          paddingRight: 20,
           backgroundColor: 'white',
-        }}
-      >
-        {filters.length > 0 && (
-          <AppliedFilters
-            filters={filters}
-            onUpdate={onUpdateFilter}
-            onDelete={onDeleteFilter}
-          />
-        )}
-      </View>
-
-      <View
-        style={{
-          backgroundColor: 'white',
-          paddingLeft: 30,
-          paddingRight: 30,
+          padding: 30,
+          paddingTop: 0,
           overflow: 'auto',
         }}
       >
diff --git a/packages/desktop-client/src/components/reports/Header.js b/packages/desktop-client/src/components/reports/Header.js
index f233f6a25404655dd1243ed0b821bf6e252553bd..3c77d5ce8c211afada82091882bdb4721f5fcde2 100644
--- a/packages/desktop-client/src/components/reports/Header.js
+++ b/packages/desktop-client/src/components/reports/Header.js
@@ -1,10 +1,9 @@
-import React from 'react';
-
 import * as monthUtils from 'loot-core/src/shared/months';
 
 import ArrowLeft from '../../icons/v1/ArrowLeft';
 import { styles } from '../../style';
 import { View, Select, Button, ButtonLink } from '../common';
+import { FilterButton, AppliedFilters } from '../filters/FiltersMenu';
 
 function validateStart(allMonths, start, end) {
   const earliest = allMonths[allMonths.length - 1].name;
@@ -52,12 +51,17 @@ function Header({
   show1Month,
   allMonths,
   onChangeDates,
-  extraButtons,
+  filters,
+  conditionsOp,
+  onApply,
+  onUpdateFilter,
+  onDeleteFilter,
+  onCondOpChange,
 }) {
   return (
     <View
       style={{
-        padding: 20,
+        padding: 10,
         paddingTop: 0,
         flexShrink: 0,
       }}
@@ -109,7 +113,7 @@ function Header({
           </Select>
         </div>
 
-        {extraButtons}
+        <FilterButton onApply={onApply} />
 
         {show1Month && (
           <Button bare onClick={() => onChangeDates(...getLatestRange(1))}>
@@ -129,6 +133,23 @@ function Header({
           All Time
         </Button>
       </View>
+      {filters.length > 0 && (
+        <View
+          style={{ marginTop: 5 }}
+          spacing={2}
+          direction="row"
+          justify="flex-start"
+          align="flex-start"
+        >
+          <AppliedFilters
+            filters={filters}
+            onUpdate={onUpdateFilter}
+            onDelete={onDeleteFilter}
+            conditionsOp={conditionsOp}
+            onCondOpChange={onCondOpChange}
+          />
+        </View>
+      )}
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/reports/NetWorth.js b/packages/desktop-client/src/components/reports/NetWorth.js
index fc3f314e0af3152e4de813cafb91dda28fe39b9c..c6a7507b0559b9fbc5ec88c59899fc33e0ac26ad 100644
--- a/packages/desktop-client/src/components/reports/NetWorth.js
+++ b/packages/desktop-client/src/components/reports/NetWorth.js
@@ -11,7 +11,6 @@ import { integerToCurrency } from 'loot-core/src/shared/util';
 
 import useFilters from '../../hooks/useFilters';
 import { styles } from '../../style';
-import { FilterButton, AppliedFilters } from '../accounts/Filters';
 import { View, P } from '../common';
 
 import Change from './Change';
@@ -24,9 +23,12 @@ import { fromDateRepr } from './util';
 function NetWorth({ accounts }) {
   const {
     filters,
+    saved,
+    conditionsOp,
     onApply: onApplyFilter,
     onDelete: onDeleteFilter,
     onUpdate: onUpdateFilter,
+    onCondOpChange,
   } = useFilters();
 
   const [allMonths, setAllMonths] = useState(null);
@@ -36,8 +38,8 @@ function NetWorth({ accounts }) {
   const [end, setEnd] = useState(monthUtils.currentMonth());
 
   const params = useMemo(
-    () => netWorthSpreadsheet(start, end, accounts, filters),
-    [start, end, accounts, filters],
+    () => netWorthSpreadsheet(start, end, accounts, filters, conditionsOp),
+    [start, end, accounts, filters, conditionsOp],
   );
   const data = useReport('net_worth', params);
 
@@ -87,34 +89,31 @@ function NetWorth({ accounts }) {
         start={start}
         end={end}
         onChangeDates={onChangeDates}
-        extraButtons={<FilterButton onApply={onApplyFilter} />}
+        filters={filters}
+        saved={saved}
+        onApply={onApplyFilter}
+        onUpdateFilter={onUpdateFilter}
+        onDeleteFilter={onDeleteFilter}
+        conditionsOp={conditionsOp}
+        onCondOpChange={onCondOpChange}
       />
 
       <View
         style={{
-          marginTop: -10,
-          paddingLeft: 20,
-          paddingRight: 20,
           backgroundColor: 'white',
-        }}
-      >
-        {filters.length > 0 && (
-          <AppliedFilters
-            filters={filters}
-            onUpdate={onUpdateFilter}
-            onDelete={onDeleteFilter}
-          />
-        )}
-      </View>
-
-      <View
-        style={{
-          backgroundColor: 'white',
-          padding: '30px',
+          padding: 30,
+          paddingTop: 0,
           overflow: 'auto',
         }}
       >
-        <View style={{ textAlign: 'right', paddingRight: 20, flexShrink: 0 }}>
+        <View
+          style={{
+            textAlign: 'right',
+            paddingTop: 20,
+            paddingRight: 20,
+            flexShrink: 0,
+          }}
+        >
           <View
             style={[styles.largeText, { fontWeight: 400, marginBottom: 5 }]}
           >
diff --git a/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.tsx b/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.tsx
index 4ac4af5745a2b31077ff9979173db482eaa2409c..b3623f6f28f0004a7f8e4d425779b83be63cdaa2 100644
--- a/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/cash-flow-spreadsheet.tsx
@@ -44,27 +44,37 @@ export function simpleCashFlow(start, end) {
   };
 }
 
-export function cashFlowByDate(start, end, isConcise, conditions = []) {
+export function cashFlowByDate(
+  start,
+  end,
+  isConcise,
+  conditions = [],
+  conditionsOp,
+) {
   return async (spreadsheet, setData) => {
     let { filters } = await send('make-filters-from-conditions', {
       conditions: conditions.filter(cond => !cond.customName),
     });
+    const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
 
     function makeQuery(where) {
-      let query = q('transactions').filter({
-        $and: [
-          ...filters,
-          { date: { $transform: '$month', $gte: start } },
-          { date: { $transform: '$month', $lte: end } },
-        ],
-        'account.offbudget': false,
-        $or: [
-          {
-            'payee.transfer_acct.offbudget': true,
-            'payee.transfer_acct': null,
-          },
-        ],
-      });
+      let query = q('transactions')
+        .filter({
+          [conditionsOpKey]: [...filters],
+        })
+        .filter({
+          $and: [
+            { date: { $transform: '$month', $gte: start } },
+            { date: { $transform: '$month', $lte: end } },
+          ],
+          'account.offbudget': false,
+          $or: [
+            {
+              'payee.transfer_acct.offbudget': true,
+              'payee.transfer_acct': null,
+            },
+          ],
+        });
 
       if (isConcise) {
         return query
@@ -84,7 +94,7 @@ export function cashFlowByDate(start, end, isConcise, conditions = []) {
       [
         q('transactions')
           .filter({
-            $and: filters,
+            [conditionsOpKey]: filters,
             date: { $transform: '$month', $lt: start },
             'account.offbudget': false,
           })
diff --git a/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx b/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx
index 99bf6ce1ebd07f915b39ded8313123bda5a3a957..dc61bdc3178b44ba7377ff350c628f947b725c28 100644
--- a/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx
@@ -19,6 +19,7 @@ export default function createSpreadsheet(
   end,
   accounts,
   conditions = [],
+  conditionsOp,
 ) {
   return async (spreadsheet, setData) => {
     if (accounts.length === 0) {
@@ -28,6 +29,7 @@ export default function createSpreadsheet(
     let { filters } = await send('make-filters-from-conditions', {
       conditions: conditions.filter(cond => !cond.customName),
     });
+    const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
 
     const data = await Promise.all(
       accounts.map(async acct => {
@@ -35,7 +37,7 @@ export default function createSpreadsheet(
           runQuery(
             q('transactions')
               .filter({
-                $and: filters,
+                [conditionsOpKey]: filters,
                 account: acct.id,
                 date: { $lt: start + '-01' },
               })
@@ -44,10 +46,12 @@ export default function createSpreadsheet(
 
           runQuery(
             q('transactions')
+              .filter({
+                [conditionsOpKey]: [...filters],
+              })
               .filter({
                 account: acct.id,
                 $and: [
-                  ...filters,
                   { date: { $gte: start + '-01' } },
                   { date: { $lte: end + '-31' } },
                 ],
diff --git a/packages/desktop-client/src/components/util/GenericInput.js b/packages/desktop-client/src/components/util/GenericInput.js
index 64f76de4e7e2aa32d90b0fe3762a87652eafdbe2..f5fd15985606cbdf79f2fb3d573620a370745d6a 100644
--- a/packages/desktop-client/src/components/util/GenericInput.js
+++ b/packages/desktop-client/src/components/util/GenericInput.js
@@ -7,6 +7,7 @@ import AccountAutocomplete from '../autocomplete/AccountAutocomplete';
 import Autocomplete from '../autocomplete/Autocomplete';
 import CategoryAutocomplete from '../autocomplete/CategorySelect';
 import PayeeAutocomplete from '../autocomplete/PayeeAutocomplete';
+import SavedFilterAutocomplete from '../autocomplete/SavedFilterAutocomplete';
 import { View, Input } from '../common';
 import { Checkbox } from '../forms';
 import DateSelect from '../select/DateSelect';
@@ -22,10 +23,9 @@ export default function GenericInput({
   style,
   onChange,
 }) {
-  let { payees, accounts, categoryGroups, dateFormat } = useSelector(state => {
+  let { saved, categoryGroups, dateFormat } = useSelector(state => {
     return {
-      payees: state.queries.payees,
-      accounts: state.queries.accounts,
+      saved: state.queries.saved,
       categoryGroups: state.queries.categories.grouped,
       dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy',
     };
@@ -47,8 +47,6 @@ export default function GenericInput({
         case 'payee':
           content = (
             <PayeeAutocomplete
-              payees={payees}
-              accounts={accounts}
               multi={multi}
               showMakeTransfer={false}
               openOnFocus={true}
@@ -65,7 +63,6 @@ export default function GenericInput({
         case 'account':
           content = (
             <AccountAutocomplete
-              accounts={accounts}
               value={value}
               multi={multi}
               openOnFocus={true}
@@ -98,6 +95,28 @@ export default function GenericInput({
       }
       break;
 
+    case 'saved':
+      switch (field) {
+        case 'saved':
+          content = (
+            <SavedFilterAutocomplete
+              saved={saved}
+              value={value}
+              multi={multi}
+              openOnFocus={true}
+              onSelect={onChange}
+              inputProps={{
+                inputRef,
+                ...(showPlaceholder ? { placeholder: 'nothing' } : null),
+              }}
+            />
+          );
+          break;
+
+        default:
+      }
+      break;
+
     case 'date':
       switch (subfield) {
         case 'month':
diff --git a/packages/desktop-client/src/hooks/useFilters.ts b/packages/desktop-client/src/hooks/useFilters.ts
index bb13ab0b35ae082727dd48b17bee4113f774aba7..617146a615b80327cbe5c9c7b8699ae4b420875c 100644
--- a/packages/desktop-client/src/hooks/useFilters.ts
+++ b/packages/desktop-client/src/hooks/useFilters.ts
@@ -2,10 +2,19 @@ import { useCallback, useMemo, useState } from 'react';
 
 export default function useFilters<T>(initialFilters: T[] = []) {
   const [filters, setFilters] = useState<T[]>(initialFilters);
+  const [conditionsOp, setConditionsOp] = useState('and');
+  const [saved, setSaved] = useState<T[]>(null);
 
   const onApply = useCallback(
-    (newFilter: T) => {
-      setFilters(state => [...state, newFilter]);
+    newFilter => {
+      if (newFilter.conditions) {
+        setFilters([...newFilter.conditions]);
+        setConditionsOp(newFilter.conditionsOp);
+        setSaved(newFilter.id);
+      } else {
+        setFilters(state => [...state, newFilter]);
+        setSaved(null);
+      }
     },
     [setFilters],
   );
@@ -15,6 +24,7 @@ export default function useFilters<T>(initialFilters: T[] = []) {
       setFilters(state =>
         state.map(f => (f === oldFilter ? updatedFilter : f)),
       );
+      setSaved(null);
     },
     [setFilters],
   );
@@ -22,17 +32,28 @@ export default function useFilters<T>(initialFilters: T[] = []) {
   const onDelete = useCallback(
     (deletedFilter: T) => {
       setFilters(state => state.filter(f => f !== deletedFilter));
+      setSaved(null);
     },
     [setFilters],
   );
 
+  const onCondOpChange = useCallback(
+    condOp => {
+      setConditionsOp(condOp);
+    },
+    [setConditionsOp],
+  );
+
   return useMemo(
     () => ({
       filters,
+      saved,
+      conditionsOp,
       onApply,
       onUpdate,
       onDelete,
+      onCondOpChange,
     }),
-    [filters, onApply, onUpdate, onDelete],
+    [filters, saved, onApply, onUpdate, onDelete, onCondOpChange, conditionsOp],
   );
 }
diff --git a/packages/loot-core/migrations/1685375406832_transaction_filters.sql b/packages/loot-core/migrations/1685375406832_transaction_filters.sql
new file mode 100644
index 0000000000000000000000000000000000000000..eb87f1c9f4deec8958d83fc24a879f88b14d16f9
--- /dev/null
+++ b/packages/loot-core/migrations/1685375406832_transaction_filters.sql
@@ -0,0 +1,10 @@
+BEGIN TRANSACTION;
+
+CREATE TABLE transaction_filters
+  (id TEXT PRIMARY KEY,
+   name TEXT,
+   conditions TEXT,
+   conditions_op TEXT DEFAULT 'and',
+   tombstone INTEGER DEFAULT 0);
+
+COMMIT;
diff --git a/packages/loot-core/src/client/data-hooks/filters.tsx b/packages/loot-core/src/client/data-hooks/filters.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..875673395f1faf782e6e6cef7eb14d36c517a846
--- /dev/null
+++ b/packages/loot-core/src/client/data-hooks/filters.tsx
@@ -0,0 +1,22 @@
+import q from '../query-helpers';
+import { useLiveQuery } from '../query-hooks';
+
+function toJS(rows) {
+  let filters = rows.map(row => {
+    return {
+      ...row.fields,
+      id: row.id,
+      name: row.name,
+      tombstone: row.tombstone,
+      conditionsOp: row.conditions_op,
+      conditions: row.conditions,
+    };
+  });
+  return filters;
+}
+
+export function useFilters() {
+  return toJS(
+    useLiveQuery(() => q('transaction_filters').select('*'), []) || [],
+  );
+}
diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/accounts/transaction-rules.ts
index fe81388007c03046c023b9668315c9eb4f96bd0a..9190bc5896e267467adbc2a919825cb4f0f3c616 100644
--- a/packages/loot-core/src/server/accounts/transaction-rules.ts
+++ b/packages/loot-core/src/server/accounts/transaction-rules.ts
@@ -62,20 +62,42 @@ function invert(obj) {
 let internalFields = schemaConfig.views.transactions.fields;
 let publicFields = invert(schemaConfig.views.transactions.fields);
 
-function fromInternalField(obj) {
+function fromInternalField<T extends { field: string }>(obj: T): T {
   return {
     ...obj,
     field: publicFields[obj.field] || obj.field,
   };
 }
 
-function toInternalField(obj) {
+function toInternalField<T extends { field: string }>(obj: T): T {
   return {
     ...obj,
     field: internalFields[obj.field] || obj.field,
   };
 }
 
+function parseArray(str) {
+  let value;
+  try {
+    value = typeof str === 'string' ? JSON.parse(str) : str;
+  } catch (e) {
+    throw new RuleError('internal', 'Cannot parse rule json');
+  }
+
+  if (!Array.isArray(value)) {
+    throw new RuleError('internal', 'Rule json must be an array');
+  }
+  return value;
+}
+
+export function parseConditionsOrActions(str) {
+  return str ? parseArray(str).map(item => fromInternalField(item)) : [];
+}
+
+export function serializeConditionsOrActions(arr) {
+  return JSON.stringify(arr.map(item => toInternalField(item)));
+}
+
 export const ruleModel = {
   validate(rule, { update }: { update?: boolean } = {}) {
     requiredFields('rules', rule, ['conditions', 'actions'], update);
@@ -99,30 +121,12 @@ export const ruleModel = {
   },
 
   toJS(row) {
-    function parseArray(str) {
-      let value;
-      try {
-        value = typeof str === 'string' ? JSON.parse(str) : str;
-      } catch (e) {
-        throw new RuleError('internal', 'Cannot parse rule json');
-      }
-
-      if (!Array.isArray(value)) {
-        throw new RuleError('internal', 'Rule json must be an array');
-      }
-      return value;
-    }
-
     let { conditions, conditions_op, actions, ...fields } = row;
     return {
       ...fields,
       conditionsOp: conditions_op,
-      conditions: conditions
-        ? parseArray(conditions).map(cond => fromInternalField(cond))
-        : [],
-      actions: actions
-        ? parseArray(actions).map(action => fromInternalField(action))
-        : [],
+      conditions: parseConditionsOrActions(conditions),
+      actions: parseConditionsOrActions(actions),
     };
   },
 
@@ -132,12 +136,10 @@ export const ruleModel = {
       row.conditions_op = conditionsOp;
     }
     if (Array.isArray(conditions)) {
-      let value = conditions.map(cond => toInternalField(cond));
-      row.conditions = JSON.stringify(value);
+      row.conditions = serializeConditionsOrActions(conditions);
     }
     if (Array.isArray(actions)) {
-      let value = actions.map(action => toInternalField(action));
-      row.actions = JSON.stringify(value);
+      row.actions = serializeConditionsOrActions(actions);
     }
     return row;
   },
diff --git a/packages/loot-core/src/server/aql/schema/index.ts b/packages/loot-core/src/server/aql/schema/index.ts
index 5f4a84acf3eaef046ac1129d436741c78e1d194e..6c56ec68946cd27f9c93a82fde7c0b540b9c5f3e 100644
--- a/packages/loot-core/src/server/aql/schema/index.ts
+++ b/packages/loot-core/src/server/aql/schema/index.ts
@@ -114,6 +114,13 @@ export const schema = {
     id: f('id'),
     note: f('string'),
   },
+  transaction_filters: {
+    id: f('id'),
+    name: f('string'),
+    conditions_op: f('string'),
+    conditions: f('json'),
+    tombstone: f('boolean'),
+  },
 };
 
 export const schemaConfig = {
diff --git a/packages/loot-core/src/server/filters/app.ts b/packages/loot-core/src/server/filters/app.ts
new file mode 100644
index 0000000000000000000000000000000000000000..748946475c9e3dfbd957efba2fd5b8e75e380554
--- /dev/null
+++ b/packages/loot-core/src/server/filters/app.ts
@@ -0,0 +1,179 @@
+import { v4 as uuidv4 } from 'uuid';
+
+import { parseConditionsOrActions } from '../accounts/transaction-rules';
+import { createApp } from '../app';
+import * as db from '../db';
+import { requiredFields } from '../models';
+import { mutator } from '../mutators';
+import { undoable } from '../undo';
+
+import { FiltersHandlers } from './types/handlers';
+
+const filterModel = {
+  validate(filter, { update }: { update?: boolean } = {}) {
+    requiredFields('transaction_filters', filter, ['conditions'], update);
+
+    if (!update || 'conditionsOp' in filter) {
+      if (!['and', 'or'].includes(filter.conditionsOp)) {
+        throw new Error('Invalid filter conditionsOp: ' + filter.conditionsOp);
+      }
+    }
+
+    return filter;
+  },
+
+  toJS(row) {
+    let { conditions, conditions_op, ...fields } = row;
+    return {
+      ...fields,
+      conditionsOp: conditions_op,
+      conditions: parseConditionsOrActions(conditions),
+    };
+  },
+
+  fromJS(filter) {
+    let { conditionsOp, ...row } = filter;
+    if (conditionsOp) {
+      row.conditions_op = conditionsOp;
+    }
+    return row;
+  },
+};
+
+async function filterNameExists(name, filterId, newItem) {
+  let idForName = await db.first(
+    'SELECT id from transaction_filters WHERE tombstone = 0 AND name = ?',
+    [name],
+  );
+
+  if (idForName === null) {
+    return false;
+  }
+  if (!newItem) {
+    return idForName.id !== filterId;
+  }
+  return true;
+}
+
+//TODO: Possible to simplify this?
+//use filters and maps
+function conditionExists(item, filters, newItem) {
+  let { conditions, conditionsOp } = item;
+  let condCheck = [];
+  let fCondCheck = false;
+  let fCondFound;
+
+  filters.map(filter => {
+    if (
+      !fCondCheck &&
+      //If conditions.length equals 1 then ignore conditionsOp
+      (conditions.length === 1 ? true : filter.conditionsOp === conditionsOp) &&
+      !filter.tombstone &&
+      filter.conditions.length === conditions.length
+    ) {
+      fCondCheck = false;
+      conditions.map((cond, i) => {
+        condCheck[i] =
+          filter.conditions.filter(fcond => {
+            return (
+              cond.value === fcond.value &&
+              cond.op === fcond.op &&
+              cond.field === fcond.field
+            );
+          }).length > 0;
+        fCondCheck = (i === 0 ? true : fCondCheck) && condCheck[i];
+        return true;
+      });
+      fCondFound = fCondCheck && condCheck[conditions.length - 1] && filter;
+    }
+    return true;
+  });
+
+  condCheck = [];
+
+  if (!newItem) {
+    return fCondFound
+      ? fCondFound.id !== item.id
+        ? fCondFound.name
+        : false
+      : false;
+  }
+  return fCondFound ? fCondFound.name : false;
+}
+
+async function createFilter(filter) {
+  let filterId = uuidv4();
+  let item = {
+    id: filterId,
+    conditions: filter.state.conditions,
+    conditionsOp: filter.state.conditionsOp,
+    name: filter.state.name,
+  };
+
+  if (item.name) {
+    if (await filterNameExists(item.name, item.id, true)) {
+      throw new Error('There is already a filter named ' + item.name);
+    }
+  } else {
+    throw new Error('Filter name is required');
+  }
+
+  if (item.conditions.length > 0) {
+    let condExists = conditionExists(item, filter.filters, true);
+    if (condExists) {
+      throw new Error(
+        'Duplicate filter warning: conditions already exist. Filter name: ' +
+          condExists,
+      );
+    }
+  } else {
+    throw new Error('Conditions are required');
+  }
+
+  // Create the filter here based on the info
+  await db.insertWithSchema('transaction_filters', filterModel.fromJS(item));
+
+  return filterId;
+}
+
+async function updateFilter(filter) {
+  let item = {
+    id: filter.state.id,
+    conditions: filter.state.conditions,
+    conditionsOp: filter.state.conditionsOp,
+    name: filter.state.name,
+  };
+  if (item.name) {
+    if (await filterNameExists(item.name, item.id, false)) {
+      throw new Error('There is already a filter named ' + item.name);
+    }
+  } else {
+    throw new Error('Filter name is required');
+  }
+
+  if (item.conditions.length > 0) {
+    let condExists = conditionExists(item, filter.filters, false);
+    if (condExists) {
+      throw new Error(
+        'Duplicate filter warning: conditions already exist. Filter name: ' +
+          condExists,
+      );
+    }
+  } else {
+    throw new Error('Conditions are required');
+  }
+
+  await db.updateWithSchema('transaction_filters', filterModel.fromJS(item));
+}
+
+async function deleteFilter(id) {
+  await db.delete_('transaction_filters', id);
+}
+
+let app = createApp<FiltersHandlers>();
+
+app.method('filter-create', mutator(createFilter));
+app.method('filter-update', mutator(updateFilter));
+app.method('filter-delete', mutator(undoable(deleteFilter)));
+
+export default app;
diff --git a/packages/loot-core/src/server/filters/types/handlers.d.ts b/packages/loot-core/src/server/filters/types/handlers.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2606be1a79d00130188fb78adf540100628cc1e9
--- /dev/null
+++ b/packages/loot-core/src/server/filters/types/handlers.d.ts
@@ -0,0 +1,7 @@
+export interface FiltersHandlers {
+  'filter-create': (filter: object) => Promise<string>;
+
+  'filter-update': (filter: object) => Promise<void>;
+
+  'filter-delete': (id: string) => Promise<void>;
+}
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index 7b060f7af734cbe5a9642d3e85ef6404ec3e61f3..a9eacee60d5bac0232fdbd013d26d33a2c89a709 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -43,6 +43,7 @@ import * as db from './db';
 import * as mappings from './db/mappings';
 import * as encryption from './encryption';
 import { APIError, TransactionError, PostError, RuleError } from './errors';
+import filtersApp from './filters/app';
 import app from './main-app';
 import { mutator, runHandler } from './mutators';
 import notesApp from './notes/app';
@@ -2309,7 +2310,7 @@ injectAPI.override((name, args) => runHandler(app.handlers[name], args));
 
 // A hack for now until we clean up everything
 app.handlers = handlers;
-app.combine(schedulesApp, budgetApp, notesApp, toolsApp);
+app.combine(schedulesApp, budgetApp, notesApp, toolsApp, filtersApp);
 
 function getDefaultDocumentDir() {
   if (Platform.isMobile) {
diff --git a/packages/loot-core/src/shared/rules.ts b/packages/loot-core/src/shared/rules.ts
index 93b2727ff9d54da234c2d0f6595faf7aa0ff9385..efcf73da0233bb1719520dc0ddee3c76209abf3b 100644
--- a/packages/loot-core/src/shared/rules.ts
+++ b/packages/loot-core/src/shared/rules.ts
@@ -11,6 +11,10 @@ export const TYPE_INFO = {
     ops: ['is', 'contains', 'oneOf'],
     nullable: true,
   },
+  saved: {
+    ops: [],
+    nullable: false,
+  },
   string: {
     ops: ['is', 'contains', 'oneOf'],
     nullable: false,
@@ -37,6 +41,7 @@ export const FIELD_TYPES = new Map(
     category: 'id',
     account: 'id',
     cleared: 'boolean',
+    saved: 'saved',
   }),
 );
 
diff --git a/packages/loot-core/src/types/handlers.d.ts b/packages/loot-core/src/types/handlers.d.ts
index 81dd0f2c118379dbf529d03f8803738c59bad5dc..802e0d3bece01757fdf3f7663c64e3bd83c6386f 100644
--- a/packages/loot-core/src/types/handlers.d.ts
+++ b/packages/loot-core/src/types/handlers.d.ts
@@ -1,4 +1,5 @@
 import type { BudgetHandlers } from '../server/budget/types/handlers';
+import type { FiltersHandlers } from '../server/filters/types/handlers';
 import type { NotesHandlers } from '../server/notes/types/handlers';
 import type { SchedulesHandlers } from '../server/schedules/types/handlers';
 import type { ToolsHandlers } from '../server/tools/types/handlers';
@@ -10,6 +11,7 @@ export interface Handlers
   extends ServerHandlers,
     ApiHandlers,
     BudgetHandlers,
+    FiltersHandlers,
     NotesHandlers,
     SchedulesHandlers,
     ToolsHandlers {}
diff --git a/upcoming-release-notes/1122.md b/upcoming-release-notes/1122.md
new file mode 100644
index 0000000000000000000000000000000000000000..64eeb36fac548e9ab4b4fcc94194e5cd13040d79
--- /dev/null
+++ b/upcoming-release-notes/1122.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [carkom]
+---
+
+Added ability to save/update/delete filters within accounts page.