diff --git a/packages/desktop-client/src/components/schedules/EditSchedule.js b/packages/desktop-client/src/components/schedules/EditSchedule.js
index fa18ba4090f54f0e9bd557f69cd1721df33a3315..8d5361dc6395adc1c5ecf4b1b3ff9d6e2b991d55 100644
--- a/packages/desktop-client/src/components/schedules/EditSchedule.js
+++ b/packages/desktop-client/src/components/schedules/EditSchedule.js
@@ -25,6 +25,7 @@ import DateSelect from '../select/DateSelect';
 import RecurringSchedulePicker from '../select/RecurringSchedulePicker';
 import { SelectedItemsButton } from '../table';
 import { AmountInput, BetweenAmountInput } from '../util/AmountInput';
+import GenericInput from '../util/GenericInput';
 
 function mergeFields(defaults, initial) {
   let res = { ...defaults };
@@ -115,6 +116,7 @@ export default function ScheduleDetails() {
               amountOp: schedule._amountOp || 'isapprox',
               date: schedule._date,
               posts_transaction: action.schedule.posts_transaction,
+              name: schedule.name,
             },
           };
         }
@@ -201,6 +203,7 @@ export default function ScheduleDetails() {
           amountOp: null,
           date: null,
           posts_transaction: false,
+          name: null,
         },
         initialFields,
       ),
@@ -341,6 +344,18 @@ export default function ScheduleDetails() {
 
   async function onSave() {
     dispatch({ type: 'form-error', error: null });
+    if (state.fields.name) {
+      let { data: sameName } = await runQuery(
+        q('schedules').filter({ name: state.fields.name }).select('id'),
+      );
+      if (sameName.length > 0 && sameName[0].id !== state.schedule.id) {
+        dispatch({
+          type: 'form-error',
+          error: 'There is already a schedule with this name',
+        });
+        return;
+      }
+    }
 
     let { error, conditions } = updateScheduleConditions(
       state.schedule,
@@ -356,6 +371,7 @@ export default function ScheduleDetails() {
       schedule: {
         id: state.schedule.id,
         posts_transaction: state.fields.posts_transaction,
+        name: state.fields.name,
       },
       conditions,
     });
@@ -431,6 +447,20 @@ export default function ScheduleDetails() {
       title={payee ? `Schedule: ${payee.name}` : 'Schedule'}
       modalSize="medium"
     >
+      <Stack direction="row" style={{ marginTop: 10 }}>
+        <FormField style={{ flex: 1 }}>
+          <FormLabel title="Schedule Name" htmlFor="name-field" />
+          <GenericInput
+            field="string"
+            type="string"
+            value={state.fields.name}
+            multi={false}
+            onChange={e => {
+              dispatch({ type: 'set-field', field: 'name', value: e });
+            }}
+          />
+        </FormField>
+      </Stack>
       <Stack direction="row" style={{ marginTop: 20 }}>
         <FormField style={{ flex: 1 }}>
           <FormLabel title="Payee" htmlFor="payee-field" />
diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.js b/packages/desktop-client/src/components/schedules/SchedulesTable.js
index 16a47be92a056f6a2d02d7b7483874ba6c04ee2c..dbc80154e960c724976f442d56088b9e7a91d4c6 100644
--- a/packages/desktop-client/src/components/schedules/SchedulesTable.js
+++ b/packages/desktop-client/src/components/schedules/SchedulesTable.js
@@ -195,6 +195,14 @@ export function SchedulesTable({
           ':hover': { backgroundColor: colors.hover },
         }}
       >
+        <Field width="flex" name="name">
+          <Text
+            style={item.name == null ? { color: colors.n8 } : null}
+            title={item.name ? item.name : ''}
+          >
+            {item.name ? item.name : 'None'}
+          </Text>
+        </Field>
         <Field width="flex" name="payee">
           <DisplayId type="payees" id={item._payee} />
         </Field>
@@ -263,6 +271,7 @@ export function SchedulesTable({
   return (
     <View style={[{ flex: 1 }, tableStyle]}>
       <TableHeader height={ROW_HEIGHT} inset={15} version="v2">
+        <Field width="flex">Name</Field>
         <Field width="flex">Payee</Field>
         <Field width="flex">Account</Field>
         <Field width={110}>Next date</Field>
diff --git a/packages/loot-core/migrations/1681115033845_add_schedule_name.sql b/packages/loot-core/migrations/1681115033845_add_schedule_name.sql
new file mode 100644
index 0000000000000000000000000000000000000000..f175f94e73e39f2b47b9f6ae2c27d5672d1ea82a
--- /dev/null
+++ b/packages/loot-core/migrations/1681115033845_add_schedule_name.sql
@@ -0,0 +1,5 @@
+BEGIN TRANSACTION;
+
+ALTER TABLE schedules ADD COLUMN name TEXT DEFAULT NULL;
+
+COMMIT;
diff --git a/packages/loot-core/src/server/aql/schema/index.ts b/packages/loot-core/src/server/aql/schema/index.ts
index ed74ff2142b1d0aee1703653a188a070e033c9a1..466de6c452daea8ccfefd1b9990a6e0f0a5a1375 100644
--- a/packages/loot-core/src/server/aql/schema/index.ts
+++ b/packages/loot-core/src/server/aql/schema/index.ts
@@ -85,6 +85,7 @@ export const schema = {
   },
   schedules: {
     id: f('id'),
+    name: f('string'),
     rule: f('id', { ref: 'rules', required: true }),
     next_date: f('date'),
     completed: f('boolean'),
diff --git a/packages/loot-core/src/server/budget/goal-template.pegjs b/packages/loot-core/src/server/budget/goal-template.pegjs
index df5b31deb68ea8f3e3748f983dbf12fbdd058438..b54b3d31c7cc21eb1218f59c65c257dfb0b15df0 100644
--- a/packages/loot-core/src/server/budget/goal-template.pegjs
+++ b/packages/loot-core/src/server/budget/goal-template.pegjs
@@ -1,7 +1,7 @@
 // https://pegjs.org
 
 expr
-  = percent: percent _ of _ category: $([^\n] *)
+  = percent: percent _ of _ category: $([^\r\n\t]+)
     { return { type: 'percentage', percent: +percent, category } }
   / amount: amount _ repeatEvery _ weeks: weekCount _ starting _ starting: date limit: limit?
     { return { type: 'week', amount, weeks, starting, limit } }
@@ -17,6 +17,8 @@ expr
     { return { type: 'simple', monthly, limit } }
   / upTo _ limit: amount
     { return { type: 'simple', limit } }
+  / schedule _ name: name
+  	{ return { type: 'schedule', name} }
 
 repeat 'repeat interval'
   = 'month'i { return { annual: false } }
@@ -39,6 +41,7 @@ of = 'of'i
 repeatEvery = 'repeat'i _ 'every'i
 starting = 'starting'i
 upTo = 'up'i _ 'to'i
+schedule = 'schedule'i
 
 _ 'space' = ' '+
 d 'digit' = [0-9]
@@ -50,3 +53,5 @@ month 'month' = $(year '-' d d)
 day 'day' = $(d d)
 date = $(month '-' day)
 currencySymbol 'currency symbol' = symbol: . & { return /\p{Sc}/u.test(symbol) }
+
+name 'Name' = $([^\r\n\t]+)
diff --git a/packages/loot-core/src/server/budget/goaltemplates.js b/packages/loot-core/src/server/budget/goaltemplates.js
index 8d490e87792b4a6ae3f93cb3d05da8e4bce91b99..67ecd5c3f124f9c04bc3575374aea2775f2f7c17 100644
--- a/packages/loot-core/src/server/budget/goaltemplates.js
+++ b/packages/loot-core/src/server/budget/goaltemplates.js
@@ -2,12 +2,18 @@ import {
   differenceInCalendarMonths,
   addMonths,
   addWeeks,
+  addDays,
   format,
 } from 'date-fns';
 
 import * as monthUtils from '../../shared/months';
+import {
+  extractScheduleConds,
+  getScheduledAmount,
+} from '../../shared/schedules';
 import { amountToInteger, integerToAmount } from '../../shared/util';
 import * as db from '../db';
+import { getRuleForSchedule, getNextDate } from '../schedules/app';
 
 import { setBudget, getSheetValue } from './actions';
 import { parse } from './goal-template.pegjs';
@@ -131,6 +137,10 @@ async function getCategoryTemplates() {
 async function applyCategoryTemplate(category, template_lines, month, force) {
   let current_month = new Date(`${month}-01`);
   let errors = [];
+  let all_schedule_names = await db.all(
+    'SELECT name from schedules WHERE name NOT NULL AND tombstone = 0',
+  );
+  all_schedule_names = all_schedule_names.map(v => v.name);
 
   // remove lines for past dates, calculate repeating dates
   template_lines = template_lines.filter(template => {
@@ -166,6 +176,12 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
           template.from = format(spend_from, 'yyyy-MM');
         }
         break;
+      case 'schedule':
+        if (!all_schedule_names.includes(template.name)) {
+          errors.push(`Schedule ${template.name} does not exist`);
+          return null;
+        }
+        break;
       default:
     }
     return true;
@@ -323,6 +339,54 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
       case 'error':
         return { errors };
       default:
+      case 'schedule': {
+        let { id: schedule_id } = await db.first(
+          'SELECT id FROM schedules WHERE name = ?',
+          [template.name],
+        );
+        let rule = await getRuleForSchedule(schedule_id);
+        let conditions = rule.serialize().conditions;
+        let { date: dateCond, amount: amountCond } =
+          extractScheduleConds(conditions);
+        let isRepeating =
+          Object(dateCond.value) === dateCond.value &&
+          'frequency' in dateCond.value;
+        let next_date_string = getNextDate(dateCond, current_month);
+        let num_months = differenceInCalendarMonths(
+          new Date(next_date_string),
+          current_month,
+        );
+        let target = -getScheduledAmount(amountCond.value);
+        let diff = target - balance + budgeted;
+        if (num_months < 0) {
+          errors.push(
+            `Non-repeating schedule ${template.name} was due on ${next_date_string}, which is in the past.`,
+          );
+          return { errors };
+        } else if (num_months > 0) {
+          if (diff >= 0 && num_months > -1) {
+            to_budget += Math.round(diff / num_months);
+          }
+        } else {
+          let monthly_target = 0;
+          let next_month = addMonths(current_month, 1);
+          let next_date = new Date(next_date_string);
+          if (isRepeating) {
+            while (next_date.getTime() < next_month.getTime()) {
+              if (next_date.getTime() >= current_month.getTime()) {
+                monthly_target += target;
+              }
+              next_date = addDays(next_date, 1);
+              next_date_string = getNextDate(dateCond, next_date);
+              next_date = new Date(next_date_string);
+            }
+          } else {
+            monthly_target = target;
+          }
+          to_budget += monthly_target - balance + budgeted;
+        }
+        break;
+      }
     }
   }
 
@@ -349,6 +413,6 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
       integerToAmount(last_month_balance + to_budget);
     str += ' ' + template_lines.map(x => x.line).join('\n');
     console.log(str);
-    return { amount: to_budget };
+    return { amount: to_budget, errors };
   }
 }
diff --git a/packages/loot-core/src/server/schedules/app.js b/packages/loot-core/src/server/schedules/app.js
index 663bb9a9d9ed3d51cb24c9ec3d69b05e6ce70bbb..dce9170f84afdbe3b908841bf174134f432e0e67 100644
--- a/packages/loot-core/src/server/schedules/app.js
+++ b/packages/loot-core/src/server/schedules/app.js
@@ -176,6 +176,20 @@ export async function setNextDate({ id, start, conditions, reset }) {
 
 // Methods
 
+export async function checkIfScheduleExists(name, scheduleId) {
+  let idForName = await db.first('SELECT id from schedules WHERE name = ?', [
+    name,
+  ]);
+
+  if (idForName == null) {
+    return false;
+  }
+  if (scheduleId) {
+    return idForName['id'] !== scheduleId;
+  }
+  return true;
+}
+
 export async function createSchedule({ schedule, conditions = [] } = {}) {
   let scheduleId = (schedule && schedule.id) || uuid.v4Sync();
 
@@ -189,6 +203,15 @@ export async function createSchedule({ schedule, conditions = [] } = {}) {
 
   let nextDate = getNextDate(dateCond);
   let nextDateRepr = nextDate ? toDateRepr(nextDate) : null;
+  if (schedule) {
+    if (schedule.name) {
+      if (await checkIfScheduleExists(schedule.name, scheduleId)) {
+        throw new Error('Cannot create schedules with the same name');
+      }
+    } else {
+      schedule.name = null;
+    }
+  }
 
   // Create the rule here based on the info
   let ruleId;
@@ -224,6 +247,13 @@ export async function updateSchedule({ schedule, conditions, resetNextDate }) {
     throw new Error('You cannot change the rule of a schedule');
   }
 
+  if (schedule.name) {
+    if (await checkIfScheduleExists(schedule.name, schedule.id)) {
+      throw new Error('There is already a schedule with this name');
+    }
+  } else {
+    schedule.name = null;
+  }
   // We need the rule if there are conditions
   let rule;
 
diff --git a/upcoming-release-notes/885.md b/upcoming-release-notes/885.md
new file mode 100644
index 0000000000000000000000000000000000000000..ddd77291e9e0f1513d9294d8d4e341be63caecd8
--- /dev/null
+++ b/upcoming-release-notes/885.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [pole95]
+---
+
+Add template keyword to budget according to named schedules