diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts
index e5e2d9e7b86c973941f8f5240321bdf237edf0a6..d26485be44aaa0b5b92604601f7d618a7a0ee39e 100644
--- a/packages/loot-core/src/server/budget/goaltemplates.ts
+++ b/packages/loot-core/src/server/budget/goaltemplates.ts
@@ -1,12 +1,11 @@
 // @ts-strict-ignore
 import { Notification } from '../../client/state-types/notifications';
 import * as monthUtils from '../../shared/months';
-import { integerToAmount, amountToInteger } from '../../shared/util';
+import { amountToInteger, integerToAmount } from '../../shared/util';
 import * as db from '../db';
 import { batchMessages } from '../sync';
 
-import { setBudget, getSheetValue, isReflectBudget, setGoal } from './actions';
-import { parse } from './goal-template.pegjs';
+import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions';
 import { goalsAverage } from './goals/goalsAverage';
 import { goalsBy } from './goals/goalsBy';
 import { goalsPercentage } from './goals/goalsPercentage';
@@ -15,9 +14,9 @@ import { goalsSchedule } from './goals/goalsSchedule';
 import { goalsSimple } from './goals/goalsSimple';
 import { goalsSpend } from './goals/goalsSpend';
 import { goalsWeek } from './goals/goalsWeek';
+import { checkTemplates, storeTemplates } from './template-notes';
 
 const TEMPLATE_PREFIX = '#template';
-const GOAL_PREFIX = '#goal';
 
 export async function applyTemplate({ month }) {
   await storeTemplates();
@@ -61,9 +60,9 @@ export function runCheckTemplates() {
 async function getCategories() {
   return await db.all(
     `
-    SELECT categories.* FROM categories 
-    INNER JOIN category_groups on categories.cat_group = category_groups.id 
-    WHERE categories.tombstone = 0 AND categories.hidden = 0 
+    SELECT categories.* FROM categories
+    INNER JOIN category_groups on categories.cat_group = category_groups.id
+    WHERE categories.tombstone = 0 AND categories.hidden = 0
     AND category_groups.hidden = 0
     `,
   );
@@ -125,27 +124,6 @@ async function resetCategoryTargets(month, category) {
   });
 }
 
-async function storeTemplates() {
-  //stores the template definitions to the database
-  const templates = await getCategoryTemplates(null);
-  const categories = await getCategories();
-
-  for (let c = 0; c < categories.length; c++) {
-    const template = templates[categories[c].id];
-    if (template) {
-      await db.update('categories', {
-        id: categories[c].id,
-        goal_def: JSON.stringify(template),
-      });
-    } else {
-      await db.update('categories', {
-        id: categories[c].id,
-        goal_def: null,
-      });
-    }
-  }
-}
-
 async function getTemplates(category, directive: string) {
   //retrieves template definitions from the database
   const goal_def = await db.all(
@@ -438,42 +416,6 @@ async function processGoals(goals, month, category?) {
     }
   }
 }
-async function getCategoryTemplates(category) {
-  const templates = {};
-
-  let notes = await db.all(
-    `
-    SELECT * FROM notes 
-    WHERE lower(note) like '%${TEMPLATE_PREFIX}%' 
-    OR lower(note) like '%${GOAL_PREFIX}%'
-     `,
-  );
-  if (category) notes = notes.filter(n => n.id === category.id);
-
-  for (let n = 0; n < notes.length; n++) {
-    const lines = notes[n].note.split('\n');
-    const template_lines = [];
-    for (let l = 0; l < lines.length; l++) {
-      const line = lines[l].trim();
-      if (
-        !line.toLowerCase().startsWith(TEMPLATE_PREFIX) &&
-        !line.toLowerCase().startsWith(GOAL_PREFIX)
-      ) {
-        continue;
-      }
-      try {
-        const parsed = parse(line);
-        template_lines.push(parsed);
-      } catch (e) {
-        template_lines.push({ type: 'error', line, error: e });
-      }
-    }
-    if (template_lines.length) {
-      templates[notes[n].id] = template_lines;
-    }
-  }
-  return templates;
-}
 
 async function applyCategoryTemplate(
   category,
@@ -709,54 +651,3 @@ async function applyCategoryTemplate(
   console.log(str);
   return { amount: to_budget, errors };
 }
-
-async function checkTemplates(): Promise<Notification> {
-  const category_templates = await getCategoryTemplates(null);
-  const errors = [];
-
-  const categories = await db.all(
-    'SELECT * FROM v_categories WHERE tombstone = 0',
-  );
-  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);
-
-  // run through each line and see if its an error
-  for (let c = 0; c < categories.length; c++) {
-    const category = categories[c];
-    const template = category_templates[category.id];
-
-    if (template) {
-      for (let l = 0; l < template.length; l++) {
-        //check for basic error
-        if (template[l].type === 'error') {
-          errors.push(category.name + ': ' + template[l].line);
-        }
-        // check schedule name error
-        if (template[l].type === 'schedule') {
-          if (!all_schedule_names.includes(template[l].name)) {
-            errors.push(
-              category.name +
-                ': Schedule “' +
-                template[l].name +
-                '” does not exist',
-            );
-          }
-        }
-      }
-    }
-  }
-  if (errors.length) {
-    return {
-      sticky: true,
-      message: `There were errors interpreting some templates:`,
-      pre: errors.join('\n\n'),
-    };
-  } else {
-    return {
-      type: 'message',
-      message: 'All templates passed! 🎉',
-    };
-  }
-}
diff --git a/packages/loot-core/src/server/budget/statements.ts b/packages/loot-core/src/server/budget/statements.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7b49ad31aa64e8ef9290af8c9e76de0a7eb0fd7e
--- /dev/null
+++ b/packages/loot-core/src/server/budget/statements.ts
@@ -0,0 +1,47 @@
+import * as db from '../db';
+import { Schedule } from '../db/types';
+
+import { GOAL_PREFIX, TEMPLATE_PREFIX } from './template-notes';
+
+/* eslint-disable rulesdir/typography */
+export async function resetCategoryGoalDefsWithNoTemplates(): Promise<void> {
+  await db.run(
+    `
+      UPDATE categories
+      SET goal_def = NULL
+      WHERE id NOT IN (SELECT n.id
+                       FROM notes n
+                       WHERE lower(note) LIKE '%${TEMPLATE_PREFIX}%'
+                          OR lower(note) LIKE '%${GOAL_PREFIX}%')
+    `,
+  );
+}
+
+/* eslint-enable rulesdir/typography */
+
+export type CategoryWithTemplateNote = {
+  id: string;
+  name: string;
+  note: string;
+};
+
+export async function getCategoriesWithTemplateNotes(): Promise<
+  CategoryWithTemplateNote[]
+> {
+  return await db.all(
+    `
+      SELECT c.id AS id, c.name as name, n.note AS note
+      FROM notes n
+             JOIN categories c ON n.id = c.id
+      WHERE c.id = n.id
+        AND (lower(note) LIKE '%${TEMPLATE_PREFIX}%'
+        OR lower(note) LIKE '%${GOAL_PREFIX}%')
+    `,
+  );
+}
+
+export async function getActiveSchedules(): Promise<Schedule[]> {
+  return await db.all(
+    'SELECT id, rule, active, completed, posts_transaction, tombstone, name from schedules WHERE name NOT NULL AND tombstone = 0',
+  );
+}
diff --git a/packages/loot-core/src/server/budget/template-notes.test.ts b/packages/loot-core/src/server/budget/template-notes.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7aaf6a99fca5b6020cec304b76dbefe6a9a4009b
--- /dev/null
+++ b/packages/loot-core/src/server/budget/template-notes.test.ts
@@ -0,0 +1,229 @@
+import * as db from '../db';
+import { Schedule } from '../db/types';
+
+import {
+  CategoryWithTemplateNote,
+  getActiveSchedules,
+  getCategoriesWithTemplateNotes,
+  resetCategoryGoalDefsWithNoTemplates,
+} from './statements';
+import { checkTemplates, storeTemplates } from './template-notes';
+
+jest.mock('../db');
+jest.mock('./statements');
+
+function mockGetTemplateNotesForCategories(
+  templateNotes: CategoryWithTemplateNote[],
+) {
+  (getCategoriesWithTemplateNotes as jest.Mock).mockResolvedValue(
+    templateNotes,
+  );
+}
+
+function mockGetActiveSchedules(schedules: Schedule[]) {
+  (getActiveSchedules as jest.Mock).mockResolvedValue(schedules);
+}
+
+function mockDbUpdate() {
+  (db.update as jest.Mock).mockResolvedValue(undefined);
+}
+
+describe('storeTemplates', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  const testCases = [
+    {
+      description: 'Stores templates for categories with valid template notes',
+      mockTemplateNotes: [
+        {
+          id: 'cat1',
+          name: 'Category 1',
+          note: '#template 10',
+        },
+      ],
+      expectedTemplates: [
+        {
+          type: 'simple',
+          monthly: 10,
+          limit: null,
+          priority: 0,
+          directive: 'template',
+        },
+      ],
+    },
+    {
+      description:
+        'Stores templates for categories with valid goal directive template notes',
+      mockTemplateNotes: [
+        {
+          id: 'cat1',
+          name: 'Category 1',
+          note: '#goal 10',
+        },
+      ],
+      expectedTemplates: [
+        {
+          type: 'simple',
+          amount: 10,
+          priority: null,
+          directive: 'goal',
+        },
+      ],
+    },
+    {
+      description: 'Does not store empty template notes',
+      mockTemplateNotes: [{ id: 'cat1', name: 'Category 1', note: '' }],
+      expectedTemplates: [],
+    },
+    {
+      description: 'Does not store non template notes',
+      mockTemplateNotes: [
+        { id: 'cat1', name: 'Category 1', note: 'Not a template note' },
+      ],
+      expectedTemplates: [],
+    },
+  ];
+
+  it.each(testCases)(
+    '$description',
+    async ({ mockTemplateNotes, expectedTemplates }) => {
+      // Given
+      mockGetTemplateNotesForCategories(mockTemplateNotes);
+      mockDbUpdate();
+
+      // When
+      await storeTemplates();
+
+      // Then
+      if (expectedTemplates.length === 0) {
+        expect(db.update).not.toHaveBeenCalled();
+        expect(resetCategoryGoalDefsWithNoTemplates).toHaveBeenCalled();
+        return;
+      }
+
+      mockTemplateNotes.forEach(({ id }) => {
+        expect(db.update).toHaveBeenCalledWith('categories', {
+          id,
+          goal_def: JSON.stringify(expectedTemplates),
+        });
+      });
+      expect(resetCategoryGoalDefsWithNoTemplates).toHaveBeenCalled();
+    },
+  );
+});
+
+describe('checkTemplates', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  const testCases = [
+    {
+      description: 'Returns success message when templates pass',
+      mockTemplateNotes: [
+        {
+          id: 'cat1',
+          name: 'Category 1',
+          note: '#template 10',
+        },
+        {
+          id: 'cat1',
+          name: 'Category 1',
+          note: '#template schedule Mock Schedule 1',
+        },
+      ],
+      mockSchedules: mockSchedules(),
+      expected: {
+        type: 'message',
+        message: 'All templates passed! 🎉',
+      },
+    },
+    {
+      description: 'Skips notes that are not templates',
+      mockTemplateNotes: [
+        {
+          id: 'cat1',
+          name: 'Category 1',
+          note: 'Not a template note',
+        },
+      ],
+      mockSchedules: mockSchedules(),
+      expected: {
+        type: 'message',
+        message: 'All templates passed! 🎉',
+      },
+    },
+    {
+      description: 'Returns errors for templates with parsing errors',
+      mockTemplateNotes: [
+        {
+          id: 'cat1',
+          name: 'Category 1',
+          note: '#template broken template',
+        },
+      ],
+      mockSchedules: mockSchedules(),
+      expected: {
+        sticky: true,
+        message: 'There were errors interpreting some templates:',
+        pre: 'Category 1: #template broken template',
+      },
+    },
+    {
+      description: 'Returns errors for non-existent schedules',
+      mockTemplateNotes: [
+        {
+          id: 'cat1',
+          name: 'Category 1',
+          note: '#template schedule Non-existent Schedule',
+        },
+      ],
+      mockSchedules: mockSchedules(),
+      expected: {
+        sticky: true,
+        message: 'There were errors interpreting some templates:',
+        pre: 'cat1: Schedule “Non-existent Schedule” does not exist',
+      },
+    },
+  ];
+
+  it.each(testCases)(
+    '$description',
+    async ({ mockTemplateNotes, mockSchedules, expected }) => {
+      // Given
+      mockGetTemplateNotesForCategories(mockTemplateNotes);
+      mockGetActiveSchedules(mockSchedules);
+
+      // When
+      const result = await checkTemplates();
+
+      // Then
+      expect(result).toEqual(expected);
+    },
+  );
+});
+
+function mockSchedules(): Schedule[] {
+  return [
+    {
+      id: 'mock-schedule-1',
+      rule: 'mock-rule',
+      active: 1,
+      completed: 0,
+      posts_transaction: 0,
+      tombstone: 0,
+      name: 'Mock Schedule 1',
+    },
+    {
+      id: 'mock-schedule-2',
+      rule: 'mock-rule',
+      active: 1,
+      completed: 0,
+      posts_transaction: 0,
+      tombstone: 0,
+      name: 'Mock Schedule 2',
+    },
+  ];
+}
diff --git a/packages/loot-core/src/server/budget/template-notes.ts b/packages/loot-core/src/server/budget/template-notes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4141ec3dc0711a01ed792bf75853e9fc9f0507c1
--- /dev/null
+++ b/packages/loot-core/src/server/budget/template-notes.ts
@@ -0,0 +1,117 @@
+import { Notification } from '../../client/state-types/notifications';
+import * as db from '../db';
+
+import { parse } from './goal-template.pegjs';
+import {
+  CategoryWithTemplateNote,
+  getActiveSchedules,
+  getCategoriesWithTemplateNotes,
+  resetCategoryGoalDefsWithNoTemplates,
+} from './statements';
+import { Template } from './types/templates';
+
+export const TEMPLATE_PREFIX = '#template';
+export const GOAL_PREFIX = '#goal';
+
+export async function storeTemplates(): Promise<void> {
+  const categoriesWithTemplates = await getCategoriesWithTemplates();
+
+  for (const { id, templates } of categoriesWithTemplates) {
+    const goalDefs = JSON.stringify(templates);
+
+    await db.update('categories', {
+      id,
+      goal_def: goalDefs,
+    });
+  }
+
+  await resetCategoryGoalDefsWithNoTemplates();
+}
+
+type CategoryWithTemplates = {
+  id: string;
+  name: string;
+  templates: Template[];
+};
+
+export async function checkTemplates(): Promise<Notification> {
+  const categoryWithTemplates = await getCategoriesWithTemplates();
+  const schedules = await getActiveSchedules();
+  const scheduleNames = schedules.map(({ name }) => name);
+  const errors: string[] = [];
+
+  categoryWithTemplates.forEach(({ id, name, templates }) => {
+    templates.forEach(template => {
+      if (template.type === 'error') {
+        errors.push(`${name}: ${template.line}`);
+      } else if (
+        template.type === 'schedule' &&
+        !scheduleNames.includes(template.name)
+      ) {
+        errors.push(`${id}: Schedule “${template.name}” does not exist`);
+      }
+    });
+  });
+
+  if (errors.length) {
+    return {
+      sticky: true,
+      message: 'There were errors interpreting some templates:',
+      pre: errors.join('\n\n'),
+    };
+  }
+
+  return {
+    type: 'message',
+    message: 'All templates passed! 🎉',
+  };
+}
+
+async function getCategoriesWithTemplates(): Promise<CategoryWithTemplates[]> {
+  const templatesForCategory: CategoryWithTemplates[] = [];
+  const templateNotes = await getCategoriesWithTemplateNotes();
+
+  templateNotes.forEach(({ id, name, note }: CategoryWithTemplateNote) => {
+    if (!note) {
+      return;
+    }
+
+    const parsedTemplates: Template[] = [];
+
+    note.split('\n').forEach(line => {
+      const trimmedLine = line.trim();
+
+      if (
+        !trimmedLine.startsWith(TEMPLATE_PREFIX) &&
+        !trimmedLine.startsWith(GOAL_PREFIX)
+      ) {
+        return;
+      }
+
+      try {
+        const parsedTemplate: Template = parse(trimmedLine);
+
+        parsedTemplates.push(parsedTemplate);
+      } catch (e: unknown) {
+        parsedTemplates.push({
+          type: 'error',
+          directive: 'error',
+          line,
+          error: (e as Error).message,
+        });
+      }
+    });
+
+    if (!parsedTemplates.length) {
+      return;
+    }
+
+    templatesForCategory.push({
+      id,
+      name,
+      templates: parsedTemplates,
+    });
+  });
+
+  return templatesForCategory;
+}
diff --git a/packages/loot-core/src/server/budget/types/templates.d.ts b/packages/loot-core/src/server/budget/types/templates.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8b6efda780d7bf07212e5beb19ddf6f89714dff7
--- /dev/null
+++ b/packages/loot-core/src/server/budget/types/templates.d.ts
@@ -0,0 +1,82 @@
+interface BaseTemplate {
+  type: string;
+  priority?: number;
+  directive: string;
+}
+
+interface PercentageTemplate extends BaseTemplate {
+  type: 'percentage';
+  percent: number;
+  previous: boolean;
+  category: string;
+}
+
+interface WeekTemplate extends BaseTemplate {
+  type: 'week';
+  amount: number;
+  weeks: number | null;
+  starting: string;
+  limit?: { amount: number; hold: boolean };
+}
+
+interface ByTemplate extends BaseTemplate {
+  type: 'by';
+  amount: number;
+  month: string;
+  repeat?: { annual: boolean; repeat?: number };
+  from?: string;
+}
+
+interface SpendTemplate extends BaseTemplate {
+  type: 'spend';
+  amount: number;
+  month: string;
+  from: string;
+  repeat?: { annual: boolean; repeat?: number };
+}
+
+interface SimpleTemplate extends BaseTemplate {
+  type: 'simple';
+  monthly?: number;
+  limit?: { amount: number; hold: boolean };
+}
+
+interface ScheduleTemplate extends BaseTemplate {
+  type: 'schedule';
+  name: string;
+  full?: boolean;
+}
+
+interface RemainderTemplate extends BaseTemplate {
+  type: 'remainder';
+  weight: number;
+  limit?: { amount: number; hold: boolean };
+}
+
+interface AverageTemplate extends BaseTemplate {
+  type: 'average';
+  amount: number;
+}
+
+interface GoalTemplate extends BaseTemplate {
+  type: 'simple';
+  amount: number;
+}
+
+interface ErrorTemplate extends BaseTemplate {
+  type: 'error';
+  line: string;
+  error: string;
+}
+
+export type Template =
+  | PercentageTemplate
+  | WeekTemplate
+  | ByTemplate
+  | SpendTemplate
+  | SimpleTemplate
+  | ScheduleTemplate
+  | RemainderTemplate
+  | AverageTemplate
+  | GoalTemplate
+  | ErrorTemplate;
diff --git a/packages/loot-core/src/server/db/types.d.ts b/packages/loot-core/src/server/db/types.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b6fdd94ec3a9b314d428453d758a62578a0f3178
--- /dev/null
+++ b/packages/loot-core/src/server/db/types.d.ts
@@ -0,0 +1,9 @@
+export type Schedule = {
+  id: string;
+  rule: string;
+  active: number;
+  completed: number;
+  posts_transaction: number;
+  tombstone: number;
+  name: string | null;
+};
diff --git a/upcoming-release-notes/3221.md b/upcoming-release-notes/3221.md
new file mode 100644
index 0000000000000000000000000000000000000000..13d9f0bb790ceb1031b9ae4c56f4d5c9e1dcf359
--- /dev/null
+++ b/upcoming-release-notes/3221.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [ ACWalker ]
+---
+
+Extract, refactor and test note handling logic from `goaltemplates.ts` file.