Skip to content

[WIP] 'until' template and improved 'starting' template#6927

Open
Gylfkxjyjdll wants to merge 22 commits intoactualbudget:masterfrom
Gylfkxjyjdll:Until-template
Open

[WIP] 'until' template and improved 'starting' template#6927
Gylfkxjyjdll wants to merge 22 commits intoactualbudget:masterfrom
Gylfkxjyjdll:Until-template

Conversation

@Gylfkxjyjdll
Copy link

@Gylfkxjyjdll Gylfkxjyjdll commented Feb 10, 2026

Add 'until' feature to templates, skip expired templates & optimized the 'starting' template. This allows more control over the templates.

  • Introduced 'until' field in template syntax to specify when a template should stop being applied.
  • Updated parsing and un-parsing logic to handle 'until' in various template types.
  • Implemented logic to skip expired templates based on the current month and 'until' date.
  • Supports YYYY-MM and YYYY-MM-DD formats for the 'starting' template
  • 'starting' can now be used everywhere

Bundle Stats

Bundle Files count Total bundle size % Changed
desktop-client 28 14.78 MB → 14.8 MB (+19.88 kB) +0.13%
loot-core 1 5.86 MB → 5.89 MB (+30 kB) +0.50%
api 1 4.4 MB → 4.42 MB (+26.7 kB) +0.59%
View detailed bundle stats

desktop-client

Total

Files count Total bundle size % Changed
28 14.78 MB → 14.8 MB (+19.88 kB) +0.13%
Changeset
File Δ Size
locale/ca.json 📈 +18.21 kB (+18.79%) 96.92 kB → 115.14 kB
locale/en.json 📈 +1.67 kB (+1.01%) 165.58 kB → 167.25 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
static/js/ca.js 96.92 kB → 115.14 kB (+18.21 kB) +18.79%
static/js/en.js 165.58 kB → 167.25 kB (+1.67 kB) +1.01%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
static/js/index.js 9.52 MB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 12.94 kB 0%
static/js/workbox-window.prod.es5.js 5.64 kB 0%
static/js/da.js 106.62 kB 0%
static/js/de.js 180.44 kB 0%
static/js/en-GB.js 7.18 kB 0%
static/js/es.js 173.83 kB 0%
static/js/fr.js 179.97 kB 0%
static/js/it.js 171.44 kB 0%
static/js/nb-NO.js 157.23 kB 0%
static/js/nl.js 106.65 kB 0%
static/js/pl.js 88.64 kB 0%
static/js/pt-BR.js 154.57 kB 0%
static/js/sv.js 78.2 kB 0%
static/js/th.js 182.35 kB 0%
static/js/uk.js 215.11 kB 0%
static/js/resize-observer.js 18.37 kB 0%
static/js/BackgroundImage.js 120.54 kB 0%
static/js/ReportRouter.js 1.13 MB 0%
static/js/narrow.js 638.75 kB 0%
static/js/TransactionList.js 106.13 kB 0%
static/js/wide.js 165.25 kB 0%
static/js/AppliedFilters.js 9.71 kB 0%
static/js/usePayeeRuleCounts.js 10.05 kB 0%
static/js/useTransactionBatchActions.js 13.23 kB 0%
static/js/FormulaEditor.js 1.04 MB 0%

loot-core

Total

Files count Total bundle size % Changed
1 5.86 MB → 5.89 MB (+30 kB) +0.50%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/budget/goal-template.pegjs 📈 +27.39 kB (+35.57%) 77.02 kB → 104.41 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/template-notes.ts 📈 +1.56 kB (+18.95%) 8.22 kB → 9.77 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/category-template-context.ts 📈 +1.05 kB (+4.46%) 23.49 kB → 24.54 kB
View detailed bundle breakdown

Added

Asset File Size % Changed
kcab.worker.IJfuY3-X.js 0 B → 5.89 MB (+5.89 MB) -

Removed

Asset File Size % Changed
kcab.worker.92NpqIpU.js 5.86 MB → 0 B (-5.86 MB) -100%

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged
No assets were unchanged


api

Total

Files count Total bundle size % Changed
1 4.4 MB → 4.42 MB (+26.7 kB) +0.59%
Changeset
File Δ Size
src/server/budget/goal-template.pegjs 📈 +24.4 kB (+36.10%) 67.6 kB → 92 kB
src/server/budget/template-notes.ts 📈 +1.38 kB (+18.77%) 7.34 kB → 8.71 kB
src/server/budget/category-template-context.ts 📈 +945 B (+4.40%) 20.99 kB → 21.92 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
bundle.api.js 4.4 MB → 4.42 MB (+26.7 kB) +0.59%

Smaller
No assets were smaller

Unchanged
No assets were unchanged

…the 'starting' templates. This allows more controll over the templates.

- Introduced 'until' field in template syntax to specify when a template should stop being applied.
- Updated parsing and un-parsing logic to handle 'until' in various template types.
- Implemented logic to skip expired templates based on the current month and 'until' date.
@actual-github-bot actual-github-bot bot changed the title 'until' template and improved 'starting' template [WIP] 'until' template and improved 'starting' template Feb 10, 2026
@netlify
Copy link

netlify bot commented Feb 10, 2026

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit 8f48f8e
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget/deploys/6991d0bfd7379f0008556d0c
😎 Deploy Preview https://deploy-preview-6927.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Contributor

👋 Hello contributor!

We would love to review your PR! Before we can do that, please make sure:

  • ✅ All CI checks pass
  • ✅ The PR is moved from draft to open (if applicable)
  • ✅ The "[WIP]" prefix is removed from the PR title
  • ✅ All CodeRabbit code review comments are resolved (if you disagree with anything - reply to the bot with your reasoning so we can read through it). The bot will eventually approve the PR.

We do this to reduce the TOIL the core contributor team has to go through for each PR and to allow for speedy reviews and merges.

For more information, please see our Contributing Guide.

@netlify
Copy link

netlify bot commented Feb 10, 2026

Deploy Preview for actualbudget-storybook ready!

Name Link
🔨 Latest commit f84f9ab
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget-storybook/deploys/6991c8bfecc6f30008f855a4
😎 Deploy Preview https://deploy-preview-6927--actualbudget-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

…the 'starting' templates. This allows more controll over the templates.

Introduced 'until' field in template syntax to specify when a template should stop being applied.

Updated parsing and un-parsing logic to handle 'until' in various template types.

Implemented logic to skip expired templates based on the current month and 'until' date.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional starting and until date fields to budget templates, updates parser and unparser to handle them, extends template types and tests, and filters out templates outside their date range early via a new isTemplateExpired helper during category-template context initialization.

Changes

Cohort / File(s) Summary
Type Definitions
packages/loot-core/src/types/models/templates.ts
Added optional starting?: string and until?: string to BaseTemplateWithPriority and RemainderTemplate.
Parser Grammar
packages/loot-core/src/server/budget/goal-template.pegjs
Added startingDate and until rules; propagated starting/until into expr alternatives so parsed templates include start/until metadata for simple, schedule, percent/average, periodic, by/spend, remainder, and copy forms.
Template Processing
packages/loot-core/src/server/budget/category-template-context.ts
Introduced isTemplateExpired(template, month) and applied it in the constructor to skip templates whose starting/until ranges exclude the current month when initializing templates, remainder, and goals.
Template Unparsing
packages/loot-core/src/server/budget/template-notes.ts
Extended unparse output to append starting <YYYY-MM> and/or until <YYYY-MM> for all template variants when those fields are present.
Tests
packages/loot-core/src/server/budget/template-notes.test.ts
Added tests and round-trip cases exercising starting and until modifiers across simple, schedule, percentage, periodic, by/spend, remainder, average, and copy templates.
Release Notes
upcoming-release-notes/6927.md
Added notes describing the new starting/until date fields and associated parser/unparser/expiry behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Parser
  participant Context
  participant Store
  participant Unparser

  Parser->>Context: parse templates (include starting/until)
  Context->>Context: isTemplateExpired(template, currentMonth)?
  alt not expired
    Context->>Store: add template to templates/remainder/goals
  else expired
    Context-->>Store: skip template
  end
  Store->>Unparser: request unparse of stored templates
  Unparser-->>Store: return textual templates including "starting" / "until"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I hop through months with careful cheer,
"Starting" here and "until" near—
I sniff each template, keep what's prime,
Skip the empty past in time,
A rabbit winks: tidy rules are clear!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title references the main change (adding 'until' field and improving 'starting' field in templates), but the [WIP] prefix and phrasing could be clearer about the primary objective.
Description check ✅ Passed The pull request description clearly relates to the changeset, explaining the introduction of 'until' field, parsing/unparsing logic updates, and template expiration logic across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/loot-core/src/server/budget/category-template-context.ts (1)

331-355: ⚠️ Potential issue | 🟠 Major

Expired templates are still passed to checkLimit and other validators.

The constructor filters expired templates out of this.templates, this.remainder, and this.goals (lines 333–336), but then passes the original unfiltered templates array to checkLimit(templates) at line 353. This means an expired limit or remainder template could still set this.limitCheck = true and enforce a spending cap even though the template is logically expired. The same applies to checkByAndScheduleAndSpend (line 75 in init), which validates the unfiltered list.

Consider filtering expired templates before passing them to checkLimit, or passing this.templates (post-filter) instead:

Proposed fix
+    const activeTemplates = templates
+      ? templates.filter(t => !CategoryTemplateContext.isTemplateExpired(t, month))
+      : [];
     // sort the template lines into regular template, goals, and remainder templates
-    if (templates) {
-      templates.forEach(t => {
-        // Skip expired templates (where month > until)
-        if (CategoryTemplateContext.isTemplateExpired(t, month)) {
-          return;
-        }
+    if (activeTemplates.length) {
+      activeTemplates.forEach(t => {
         if (
           t.directive === 'template' &&
           t.type !== 'remainder' &&
@@ ..
       });
     }

-    this.checkLimit(templates);
+    this.checkLimit(activeTemplates);
     this.checkSpend();
     this.checkGoal();

And similarly in init, pass activeTemplates to checkByAndScheduleAndSpend.

🤖 Fix all issues with AI agents
In `@packages/loot-core/src/server/budget/goal-template.pegjs`:
- Around line 4-5: The PEG grammar's rawScheduleName and name rules are
currently too greedy and can absorb the "starting" or "until" keywords into the
name/category (e.g., in the percentage branch returned by the template
production), so update both rules to add negative lookaheads that prevent
matching when the next token is the reserved keywords "starting" or "until";
specifically modify the rawScheduleName rule and the name rule used in the
percentage branch so they assert (via negative lookahead) that the upcoming text
is not "starting" or "until" before consuming characters, ensuring starting: and
until: productions are matched correctly in functions/productions like template
and the percentage branch.
🧹 Nitpick comments (1)
packages/loot-core/src/server/budget/template-notes.ts (1)

170-175: Consider extracting the repeated starting/until suffix pattern.

The same 4-line block (if (template.starting) ... if (template.until) ...) is duplicated 8 times across all template branches. A small helper would reduce repetition and keep the branches focused on template-specific logic.

♻️ Example helper extraction
+function appendDateBounds(
+  result: string,
+  template: { starting?: string; until?: string },
+): string {
+  if (template.starting) {
+    result += ` starting ${template.starting}`;
+  }
+  if (template.until) {
+    result += ` until ${template.until}`;
+  }
+  return result;
+}

Then each branch simplifies to:

-          if (template.starting) {
-            result += ` starting ${template.starting}`;
-          }
-          if (template.until) {
-            result += ` until ${template.until}`;
-          }
-          return result.trim();
+          return appendDateBounds(result, template).trim();

Also applies to: 192-197, 204-210, 219-221, 238-243, 255-260, 275-280, 286-291

@github-actions
Copy link
Contributor

🤖 Auto-generated Release Notes

Hey @Gylfkxjyjdll! I've automatically created a release notes file based on CodeRabbit's analysis:

Category: Features
Summary: Add optional 'starting' and 'until' date fields to budget templates for improved temporal management.
File: upcoming-release-notes/6927.md

If you're happy with this release note, you can add it to your pull request. If not, you'll need to add your own before a maintainer can review your change.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@upcoming-release-notes/6927.md`:
- Line 6: Replace the verbose, technical release note with a single, user-facing
sentence that summarizes the benefit: mention that templates now support an
"until" date and that template start behavior is more flexible (refer to the
'until' and 'starting' features) and remove implementation details like "parsing
and un-parsing logic"; ensure the sentence is non-technical, concise, and fits
the upcoming-release-notes/*.md one-line rule for Enhancements.

…ules were too eager and would absorb 'starting' or 'until'. Added negative lookaheads to prevent this, ensuring correct parsing.
@netlify
Copy link

netlify bot commented Feb 10, 2026

Deploy Preview for actualbudget-website ready!

Name Link
🔨 Latest commit f84f9ab
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget-website/deploys/6991c8bff0dddf000836c6d5
😎 Deploy Preview https://deploy-preview-6927.www.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/loot-core/src/server/budget/goal-template.pegjs`:
- Around line 101-116: The rawScheduleName rule's negative lookaheads use `_`
(optional whitespace) so they wrongly trigger mid-word (e.g., "restarting");
update the lookaheads in rawScheduleName to require mandatory whitespace before
keywords (use `__` instead of `_`) and ensure the keyword is followed by either
whitespace or end-of-input (e.g., check `'starting'i`/`'until'i` are followed by
`__` or end via `(__ / !.)` or an equivalent lookahead), and apply the same
change for both the initial and continuation checks (the parts referencing `_
'starting'i`/`_ 'until'i` and the continuation that uses `[^\r\n]`) so schedule
names containing those substrings are not cut mid-word.
- Around line 118-125: The per-character negative lookaheads in the PEG rule
cause early stops inside words (e.g., "restarting"); update the lookaheads in
the name rule (and the rawScheduleName rules) to only trigger when the keyword
is preceded by whitespace by checking for a space/tab before the keyword in the
lookahead (i.e., replace !('starting'i _) and !('until'i _) with a negative
lookahead that looks for [ \t] (or explicit space) immediately before the
keyword plus the following separator, so the check only fires for standalone "
starting" / " until" and not mid-word occurrences.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 10, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 10, 2026
@Gylfkxjyjdll
Copy link
Author

Actionable comments posted: 1
🤖 Fix all issues with AI agents

This makes it very easy. thx bot =)).

@Gylfkxjyjdll Gylfkxjyjdll changed the title [WIP] 'until' template and improved 'starting' template 'until' template and improved 'starting' template Feb 10, 2026
Copy link
Member

@youngcw youngcw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't done a thorough test yet, but it looks quite good so far. Thanks for not leaving a pile of AI spaghetti.

@@ -442,6 +445,26 @@ export class CategoryTemplateContext {
});
}

private static isTemplateExpired(template: Template, month: string): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name here probably should be something more general as expired sounds like you are only doing the "until" check.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look later on it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, name should be more specific now.

@@ -331,6 +330,10 @@ export class CategoryTemplateContext {
// sort the template lines into regular template, goals, and remainder templates
if (templates) {
templates.forEach(t => {
// Skip expired templates (where month > until)
if (CategoryTemplateContext.isTemplateExpired(t, month)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is simpler then I would have thought. I was worried this would have to touch a lot of things to work.

= template: template _ percentOf:percentOf category: name starting: startingDate? until:until?
{ return { type: 'percentage', percent: +percentOf.percent, previous: percentOf.prev, category, starting, until, priority: template.priority, directive: template.directive }}
/ template: template _ amount: amount _ repeatEvery _ period: periodCount _ starting: startingDate limit: limit? until: until?
{ return { type: 'periodic', amount, period, starting, limit, until, priority: template.priority, directive: template.directive }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if you would catch that there was already a start date here 🙂

@youngcw
Copy link
Member

youngcw commented Feb 10, 2026

Oh, this needs documentation before merging.

@youngcw
Copy link
Member

youngcw commented Feb 10, 2026

@jfdoming Any thoughts?

@Gylfkxjyjdll
Copy link
Author

My apologies for the potentially confusing naming convention. The concept was initially 'until' but was later expanded to 'starting'. I had considered using 'ending' as an alternative. My coding skills are a bit rusty x)

The utility of this feature lies in providing greater control over budgeting by allowing it to be "scripted", thereby enabling planning for the entire year.

A future enhancement could involve a "budget carryover" template.

/ template: template _ amount: amount _ by _ month: month from: spendFrom? repeat: (_ repeatEvery _ repeat)?
= template: template _ percentOf:percentOf category: name starting: startingDate? until:until?
{ return { type: 'percentage', percent: +percentOf.percent, previous: percentOf.prev, category, starting, until, priority: template.priority, directive: template.directive }}
/ template: template _ amount: amount _ repeatEvery _ period: periodCount _ starting: startingDate limit: limit? until: until?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks existing functionality. Weekly and daily periodic templates need a specific day to start on. If there is a clean way to force the start date to have a day on weekly and daily versions then we are fine. If not, allowing YYYY-MM dates needs to be re-evaluated.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right... I didn't think about that. I need to think about this and look at it more closely.
The YYYY-MM format should be a convenience function that makes things intuitive when you have to write a lot. And the syntax "starting YYYY-MM until YYYY-MM" remains consistent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just the periodic template could require a day for the starting value. I would think the timeframe check would still work basically the same if the starting value has a day for those.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the repeat template:

      // Wenn "repeat every week/day" und "starting" ist  YYYY-MM = 7 Zeichen, ergänze -01
      starting: (period.period === 'week' || period.period === 'day') && starting?.length === 7 ? starting + '-01' : starting,

But i have to think about, if this will work in all cases..

And if this is needed in the until template as well..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to overthink this and will take some time. Because repeat every day starting 2026-01-01 until 2026-01-05 still calculates the whole month. didn't think about this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only allowing the until and starting values to be months makes sense for everything but the periodic template. If just the periodic template can be fixed then you should be good

@github-actions
Copy link
Contributor

VRT tests ❌ failed. View the test report.

To update the VRT screenshots, comment /update-vrt on this PR. The VRT update operation takes about 50 minutes.

@Gylfkxjyjdll Gylfkxjyjdll changed the title 'until' template and improved 'starting' template [WIP] 'until' template and improved 'starting' template Feb 11, 2026
@Gylfkxjyjdll
Copy link
Author

tbh i had some trouble with the periodic starting and until - i had a very hard time. It needed a very, very specific syntax to work and was unintuitive. And if something is implemented, it should "always" work or giving feedback to the user what's wrong. but i have to think about the logic of the templates.. if this make sense =/

@Gylfkxjyjdll
Copy link
Author

I used ChatGPT to optimize runPeriodic(). Had some trouble making the template usage consistent, but it should be working now.

Feedback welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants