diff --git a/packages/desktop-client/src/components/settings/Experimental.js b/packages/desktop-client/src/components/settings/Experimental.js
index f3e96b5ad03ccf3467c1387f8b7b2c30563f413c..a96411f34da3f0b3fe021469813a70eb1ec9f8e5 100644
--- a/packages/desktop-client/src/components/settings/Experimental.js
+++ b/packages/desktop-client/src/components/settings/Experimental.js
@@ -14,6 +14,7 @@ export default function ExperimentalFeatures({ prefs, savePrefs }) {
       .map(([key, value]) => [key.replace('flags.', ''), value])
   );
   let disabled = prefs.budgetType === 'report' && flags.reportBudget;
+
   return (
     <Setting
       primaryAction={
@@ -45,7 +46,7 @@ export default function ExperimentalFeatures({ prefs, savePrefs }) {
 
             <label style={{ display: 'flex' }}>
               <Checkbox
-                id="report-budget-flag"
+                id="sync-account-flag"
                 checked={flags.syncAccount}
                 onChange={() => {
                   savePrefs({ 'flags.syncAccount': !flags.syncAccount });
@@ -53,6 +54,18 @@ export default function ExperimentalFeatures({ prefs, savePrefs }) {
               />{' '}
               <View>Enable account syncing</View>
             </label>
+            <label style={{ display: 'flex' }}>
+              <Checkbox
+                id="goal-templates-flag"
+                checked={flags.goalTemplatesEnabled}
+                onChange={() => {
+                  savePrefs({
+                    'flags.goalTemplatesEnabled': !flags.goalTemplatesEnabled
+                  });
+                }}
+              />{' '}
+              <View>Enable Goal Templates</View>
+            </label>
           </View>
         ) : (
           <Link
diff --git a/packages/loot-core/src/client/actions/queries.js b/packages/loot-core/src/client/actions/queries.js
index 8068224ceca625ed16fb1645af656114dfa42427..0a785d83289b2a619285d4cbff8a4929b990a7d5 100644
--- a/packages/loot-core/src/client/actions/queries.js
+++ b/packages/loot-core/src/client/actions/queries.js
@@ -25,6 +25,12 @@ export function applyBudgetAction(month, type, args) {
       case 'set-3-avg':
         await send('budget/set-3month-avg', { month });
         break;
+      case 'apply-goal-template':
+        await send('budget/apply-goal-template', { month });
+        break;
+      case 'overwrite-goal-template':
+        await send('budget/overwrite-goal-template', { month });
+        break;
       case 'hold':
         await send('budget/hold-for-next-month', {
           month,
diff --git a/packages/loot-core/src/server/budget/actions.js b/packages/loot-core/src/server/budget/actions.js
index 47f98f1cc19aa931410408eed99e20ed387d58fc..ae6c7bff085670c90f4cdc3e22ddf7a039d2d33c 100644
--- a/packages/loot-core/src/server/budget/actions.js
+++ b/packages/loot-core/src/server/budget/actions.js
@@ -5,7 +5,7 @@ import * as prefs from '../prefs';
 import * as sheet from '../sheet';
 import { batchMessages } from '../sync';
 
-async function getSheetValue(sheetName, cell) {
+export async function getSheetValue(sheetName, cell) {
   const node = await sheet.getCell(sheetName, cell);
   return safeNumber(typeof node.value === 'number' ? node.value : 0);
 }
diff --git a/packages/loot-core/src/server/budget/app.js b/packages/loot-core/src/server/budget/app.js
index 99766c8a5c333cd40cb89a04f2b3c8cc1a5e0f4a..ed8c35e0bfd4e998feb42ac236a437bf45ef0a05 100644
--- a/packages/loot-core/src/server/budget/app.js
+++ b/packages/loot-core/src/server/budget/app.js
@@ -3,6 +3,7 @@ import { mutator } from '../mutators';
 import { undoable } from '../undo';
 
 import * as actions from './actions';
+import * as goalActions from './goaltemplates';
 
 let app = createApp();
 
@@ -13,6 +14,14 @@ app.method(
 );
 app.method('budget/set-zero', mutator(undoable(actions.setZero)));
 app.method('budget/set-3month-avg', mutator(undoable(actions.set3MonthAvg)));
+app.method(
+  'budget/apply-goal-template',
+  mutator(undoable(goalActions.applyTemplate))
+);
+app.method(
+  'budget/overwrite-goal-template',
+  mutator(undoable(goalActions.overwriteTemplate))
+);
 app.method(
   'budget/hold-for-next-month',
   mutator(undoable(actions.holdForNextMonth))
diff --git a/packages/loot-core/src/server/budget/goaltemplates.js b/packages/loot-core/src/server/budget/goaltemplates.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4ec2db12b7be2e70a9a49e3c643e52f98dbd8e2
--- /dev/null
+++ b/packages/loot-core/src/server/budget/goaltemplates.js
@@ -0,0 +1,443 @@
+import {
+  differenceInCalendarMonths,
+  addMonths,
+  addWeeks,
+  format
+} from 'date-fns';
+
+import * as monthUtils from '../../shared/months';
+import { amountToInteger, integerToAmount } from '../../shared/util';
+import * as db from '../db';
+
+import { setBudget, getSheetValue } from './actions';
+
+export async function applyTemplate({ month }) {
+  await processTemplate(month, false);
+}
+
+export async function overwriteTemplate({ month }) {
+  await processTemplate(month, true);
+}
+
+async function processTemplate(month, force) {
+  let category_templates = await getCategoryTemplates();
+
+  let categories = await db.all(
+    'SELECT * FROM v_categories WHERE tombstone = 0'
+  );
+
+  let num_applied = 0;
+  for (let c = 0; c < categories.length; c++) {
+    let category = categories[c];
+
+    let budgeted = await getSheetValue(
+      monthUtils.sheetForMonth(month),
+      `budget-${category.id}`
+    );
+
+    if (budgeted === 0 || force) {
+      let template = category_templates[category.id];
+
+      if (template) {
+        let to_budget = await applyCategoryTemplate(
+          category,
+          template,
+          month,
+          force
+        );
+        if (to_budget != null) {
+          num_applied++;
+          await setBudget({ category: category.id, month, amount: to_budget });
+        }
+      }
+    }
+  }
+  if (num_applied === 0) {
+    console.log('All categories were up to date.');
+  } else {
+    console.log(`${num_applied} categories updated.`);
+  }
+}
+
+async function getCategoryTemplates() {
+  const matches = [
+    {
+      type: 'simple',
+        re: /^#template \$?(\-?\d+(\.\d{2})?)$/im,//eslint-disable-line
+      params: ['monthly']
+    },
+    {
+      type: 'simple',
+      re: /^#template up to \$?(\d+(\.\d{2})?)$/im,
+      params: ['limit']
+    },
+    {
+      type: 'simple',
+      re: /^#template \$?(\d+(\.\d{2})?) up to \$?(\d+(\.\d{2})?)$/im,
+      params: ['monthly', null, 'limit']
+    },
+    {
+      type: 'by',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2})$/im,//eslint-disable-line
+      params: ['amount', null, 'month']
+    },
+    {
+      type: 'by',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2}) repeat every (\d+) months$/im,//eslint-disable-line
+      params: ['amount', null, 'month', 'repeat']
+    },
+    {
+      type: 'week',
+        re: /^#template \$?(\d+(\.\d{2})?) repeat every week starting (\d{4}\-\d{2}\-\d{2})$/im,//eslint-disable-line
+      params: ['amount', null, 'starting']
+    },
+    {
+      type: 'week',
+        re: /^#template \$?(\d+(\.\d{2})?) repeat every week starting (\d{4}\-\d{2}\-\d{2}) up to \$?(\d+(\.\d{2})?)$/im,//eslint-disable-line
+      params: ['amount', null, 'starting', 'limit']
+    },
+    {
+      type: 'weeks',
+        re: /^#template \$?(\d+(\.\d{2})?) repeat every (\d+) weeks starting (\d{4}\-\d{2}\-\d{2})$/im,//eslint-disable-line
+      params: ['amount', null, 'weeks', 'starting']
+    },
+    {
+      type: 'weeks',
+        re: /^#template \$?(\d+(\.\d{2})?) repeat every (\d+) weeks starting (\d{4}\-\d{2}\-\d{2}) up to \$?(\d+(\.\d{2})?)$/im,//eslint-disable-line
+      params: ['amount', null, 'weeks', 'starting', 'limit']
+    },
+    {
+      type: 'by_annual',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2}) repeat every year$/im,//eslint-disable-line
+      params: ['amount', null, 'month']
+    },
+    {
+      type: 'by_annual',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2}) repeat every (\d+) years$/im,//eslint-disable-line
+      params: ['amount', null, 'month', 'repeat']
+    },
+    {
+      type: 'spend',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2}) spend from (\d{4}\-\d{2})$/im,//eslint-disable-line
+      params: ['amount', null, 'month', 'from']
+    },
+    {
+      type: 'spend',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2}) spend from (\d{4}\-\d{2}) repeat every (\d+) months$/im,//eslint-disable-line
+      params: ['amount', null, 'month', 'from', 'repeat']
+    },
+    {
+      type: 'spend_annual',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2}) spend from (\d{4}\-\d{2}) repeat every year$/im,//eslint-disable-line
+      params: ['amount', null, 'month', 'from']
+    },
+    {
+      type: 'spend_annual',
+        re: /^#template \$?(\d+(\.\d{2})?) by (\d{4}\-\d{2}) spend from (\d{4}\-\d{2}) repeat every (\d+) years$/im,//eslint-disable-line
+      params: ['amount', null, 'month', 'from', 'repeat']
+    },
+    {
+      type: 'percentage',
+      re: /^#template (\d+(\.\d+)?)% of (.*)$/im,
+      params: ['percent', null, 'category']
+    },
+    { type: 'error', re: /^#template .*$/im, params: [] }
+  ];
+
+  let templates = {};
+
+  let notes = await db.all(`SELECT * FROM notes WHERE note like '%#template%'`);
+
+  for (let n = 0; n < notes.length; n++) {
+    let lines = notes[n].note.split('\n');
+    let template_lines = [];
+    for (let l = 0; l < lines.length; l++) {
+      for (let m = 0; m < matches.length; m++) {
+        let arr = matches[m].re.exec(lines[l]);
+        if (arr) {
+          let matched = {};
+          matched.line = arr[0];
+          matched.type = matches[m].type;
+          for (let p = 0; p < matches[m].params.length; p++) {
+            let param_name = matches[m].params[p];
+            if (param_name) {
+              matched[param_name] = arr[p + 1];
+            }
+          }
+          template_lines.push(matched);
+          break;
+        }
+      }
+    }
+    if (template_lines.length) {
+      templates[notes[n].id] = template_lines;
+    }
+  }
+  return templates;
+}
+
+async function applyCategoryTemplate(category, template_lines, month, force) {
+  let current_month = new Date(`${month}-01`);
+
+  // remove lines for past dates, calculate repeating dates
+  let got_by = false;
+  template_lines = template_lines.filter(template => {
+    //debugger;
+
+    switch (template.type) {
+      case 'by':
+      case 'by_annual':
+      case 'spend':
+      case 'spend_annual':
+        let target_month = new Date(`${template.month}-01`);
+        let num_months = differenceInCalendarMonths(
+          target_month,
+          current_month
+        );
+        let repeat = template.type.includes('annual')
+          ? (template.repeat || 1) * 12
+          : template.repeat;
+
+        let spend_from;
+        if (template.type.includes('spend')) {
+          spend_from = new Date(`${template.from}-01`);
+        }
+        while (num_months < 0 && repeat) {
+          target_month = addMonths(target_month, repeat);
+          if (spend_from) {
+            spend_from = addMonths(spend_from, repeat);
+          }
+          num_months = differenceInCalendarMonths(target_month, current_month);
+        }
+        if (num_months < 0) {
+          console.log(
+            `${category.name}: ${`${template.month} is in the past:`} ${
+              template.line
+            }`
+          );
+          return null;
+        }
+        template.month = format(target_month, 'yyyy-MM');
+        if (spend_from) {
+          template.from = format(spend_from, 'yyyy-MM');
+        }
+        break;
+      default:
+    }
+    return template;
+  });
+
+  if (template_lines.length > 1) {
+    template_lines = template_lines
+      .sort((a, b) => {
+        if (
+          a.type.slice(0, 2) === b.type.slice(0, 2) &&
+          a.type.slice(0, 2) === 'by'
+        ) {
+          return differenceInCalendarMonths(
+            new Date(`${a.month}-01`),
+            new Date(`${b.month}-01`)
+          );
+        } else {
+          return a.type.localeCompare(b.type);
+        }
+      })
+      .filter(el => {
+        if (el.type.slice(0, 2) === 'by') {
+          if (!got_by) {
+            got_by = true;
+            return el;
+          } else {
+            return null;
+          }
+        } else {
+          return el;
+        }
+      });
+  }
+
+  let to_budget = 0;
+  let limit;
+  let sheetName = monthUtils.sheetForMonth(month);
+  let budgeted = await getSheetValue(sheetName, `budget-${category.id}`);
+  let spent = await getSheetValue(sheetName, `sum-amount-${category.id}`);
+  let balance = await getSheetValue(sheetName, `leftover-${category.id}`);
+  let last_month_balance = balance - spent - budgeted;
+  for (let l = 0; l < template_lines.length; l++) {
+    let template = template_lines[l];
+    switch (template.type) {
+      case 'simple': {
+        // simple has 'monthly' and/or 'limit' params
+        if (template.limit != null) {
+          if (limit != null) {
+            console.log(
+              `${category.name}: ${`More than one 'up to' limit found.`} ${
+                template.line
+              }`
+            );
+            return null;
+          } else {
+            limit = amountToInteger(template.limit);
+          }
+        }
+        if (template.monthly) {
+          let monthly = amountToInteger(template.monthly);
+          to_budget += monthly;
+        } else {
+          to_budget += limit;
+        }
+        break;
+      }
+      case 'by':
+      case 'by_annual': {
+        // by has 'amount' and 'month' params
+        let target_month = new Date(`${template.month}-01`);
+        let target = amountToInteger(template.amount);
+        let num_months = differenceInCalendarMonths(
+          target_month,
+          current_month
+        );
+        let repeat =
+          template.type === 'by'
+            ? template.repeat
+            : (template.repeat || 1) * 12;
+        while (num_months < 0 && repeat) {
+          target_month = addMonths(target_month, repeat);
+          num_months = differenceInCalendarMonths(target_month, current_month);
+        }
+        let diff = target - last_month_balance;
+        if (diff >= 0 && num_months > -1) {
+          to_budget += Math.round(diff / (num_months + 1));
+        }
+        break;
+      }
+      case 'week':
+      case 'weeks': {
+        // weeks has 'amount', 'starting' and optional 'limit' params
+        // weeks has 'amount', 'starting', 'weeks' and optional 'limit' params
+        let amount = amountToInteger(template.amount);
+        let weeks = template.weeks != null ? Math.round(template.weeks) : 1;
+        if (template.limit != null) {
+          if (limit != null) {
+            console.log(
+              `${category.name}: ${`More than one 'up to' limit found.`} ${
+                template.line
+              }`
+            );
+            return null;
+          } else {
+            limit = amountToInteger(template.limit);
+          }
+        }
+        let w = new Date(template.starting);
+
+        let next_month = addMonths(current_month, 1);
+
+        while (w.getTime() < next_month.getTime()) {
+          if (w.getTime() >= current_month.getTime()) {
+            to_budget += amount;
+          }
+          w = addWeeks(w, weeks);
+        }
+        break;
+      }
+      case 'spend':
+      case 'spend_annual': {
+        // spend has 'amount' and 'from' and 'month' params
+        let from_month = new Date(`${template.from}-01`);
+        let to_month = new Date(`${template.month}-01`);
+        let already_budgeted = last_month_balance;
+        let first_month = true;
+        for (
+          let m = from_month;
+          differenceInCalendarMonths(current_month, m) > 0;
+          m = addMonths(m, 1)
+        ) {
+          let sheetName = monthUtils.sheetForMonth(format(m, 'yyyy-MM'));
+
+          if (first_month) {
+            let spent = await getSheetValue(
+              sheetName,
+              `sum-amount-${category.id}`
+            );
+            let balance = await getSheetValue(
+              sheetName,
+              `leftover-${category.id}`
+            );
+            already_budgeted = balance - spent;
+            first_month = false;
+          } else {
+            let budgeted = await getSheetValue(
+              sheetName,
+              `budget-${category.id}`
+            );
+            already_budgeted += budgeted;
+          }
+        }
+        let num_months = differenceInCalendarMonths(to_month, current_month);
+        let target = amountToInteger(template.amount);
+        if (num_months < 0) {
+          console.log(
+            `${category.name}: ${`${template.to} is in the past:`} ${
+              template.line
+            }`
+          );
+          return null;
+        } else if (num_months === 0) {
+          to_budget = target - already_budgeted;
+        } else {
+          to_budget = Math.round(
+            (target - already_budgeted) / (num_months + 1)
+          );
+        }
+        break;
+      }
+      case 'percentage': {
+        /*
+          let income_category = (await actual.getCategories()).filter(c => c.is_income == true && c.name == template.category);
+          let func = (getBudgetMonthTestFunc || getBudgetMonth);
+          let budget = await func(month);
+          for (var g = 0; g < budget.categoryGroups.length; g++) {
+            if (income_category.group_id == budget.categoryGroups[g].id) {
+              for (var c = 0; c < budget.categoryGroups[g].categories.length; c++)
+                if (income_category.id == budget.categoryGroups[g].categories[c].id) {
+                  let month_category = budget.categoryGroups[g].categories[c];
+                }
+            }
+          }
+          */
+        break;
+      }
+      case 'error':
+        console.log(`${category.name}: ${`Failed to match:`} ${template.line}`);
+        return null;
+      default:
+    }
+  }
+
+  if (limit != null) {
+    if (to_budget + last_month_balance > limit) {
+      to_budget = limit - last_month_balance;
+    }
+  }
+
+  if (
+    ((category.budgeted != null && category.budgeted !== 0) ||
+      to_budget === 0) &&
+    !force
+  ) {
+    return null;
+  } else if (category.budgeted === to_budget && force) {
+    return null;
+  } else {
+    let str = category.name + ': ' + integerToAmount(last_month_balance);
+    str +=
+      ' + ' +
+      integerToAmount(to_budget) +
+      ' = ' +
+      integerToAmount(last_month_balance + to_budget);
+    str += ' ' + template_lines.map(x => x.line).join('\n');
+    console.log(str);
+    return to_budget;
+  }
+}
diff --git a/packages/loot-design/src/components/budget/rollover/BudgetSummary.js b/packages/loot-design/src/components/budget/rollover/BudgetSummary.js
index 74bb5a788d3cf260499471a19ab1667c15a2cbc3..39479aa7546958d3b5953e670848303cd3776fa5 100644
--- a/packages/loot-design/src/components/budget/rollover/BudgetSummary.js
+++ b/packages/loot-design/src/components/budget/rollover/BudgetSummary.js
@@ -1,4 +1,5 @@
 import React, { useState } from 'react';
+import { connect } from 'react-redux';
 
 import Component from '@reactions/component';
 import { css } from 'glamor';
@@ -6,6 +7,7 @@ import { css } from 'glamor';
 import { rolloverBudget } from 'loot-core/src/client/queries';
 import * as monthUtils from 'loot-core/src/shared/months';
 
+import * as actions from '../../../../../loot-core/src/client/actions';
 import { colors, styles } from '../../../style';
 import DotsHorizontalTriple from '../../../svg/v1/DotsHorizontalTriple';
 import ArrowButtonDown1 from '../../../svg/v2/ArrowButtonDown1';
@@ -243,7 +245,7 @@ function ToBudget({ month, prevMonthName, collapsed, onBudgetAction }) {
   );
 }
 
-export default React.memo(function BudgetSummary({ month }) {
+function BudgetSummary({ month, localPrefs }) {
   let {
     currentMonth,
     summaryCollapsed: collapsed,
@@ -264,6 +266,8 @@ export default React.memo(function BudgetSummary({ month }) {
 
   let ExpandOrCollapseIcon = collapsed ? ArrowButtonDown1 : ArrowButtonUp1;
 
+  let goalTemplatesEnabled = localPrefs['flags.goalTemplatesEnabled'];
+
   return (
     <View
       style={{
@@ -373,6 +377,14 @@ export default React.memo(function BudgetSummary({ month }) {
                       {
                         name: 'set-3-avg',
                         text: 'Set budgets to 3 month avg'
+                      },
+                      goalTemplatesEnabled && {
+                        name: 'apply-goal-template',
+                        text: 'Apply budget template'
+                      },
+                      goalTemplatesEnabled && {
+                        name: 'overwrite-goal-template',
+                        text: 'Overwrite with budget template'
                       }
                     ]}
                   />
@@ -410,4 +422,9 @@ export default React.memo(function BudgetSummary({ month }) {
       </NamespaceContext.Provider>
     </View>
   );
-});
+}
+
+export default connect(
+  state => ({ localPrefs: state.prefs.local }),
+  actions
+)(BudgetSummary);