Skip to content

[AI] Add BALANCE_OF() function to fetch balance of an arbitrary account#7335

Open
StephenBrown2 wants to merge 17 commits intoactualbudget:masterfrom
StephenBrown2:push-mmnmmrvoszkr
Open

[AI] Add BALANCE_OF() function to fetch balance of an arbitrary account#7335
StephenBrown2 wants to merge 17 commits intoactualbudget:masterfrom
StephenBrown2:push-mmnmmrvoszkr

Conversation

@StephenBrown2
Copy link
Copy Markdown
Contributor

@StephenBrown2 StephenBrown2 commented Mar 30, 2026

Description

This PR adds BALANCE_OF("…") for rule formulas (the ƒ formula mode on Set actions and split-amount formulas).

  • What it does: You can reference the running balance of another account (in cents, same semantics as the existing balance variable) at the same transaction cutoff as the current row (date, sort_order, id ordering).
  • How the argument is resolved: The quoted string is matched first as an account id (if it exists as a key in the accounts map), then as an exact account name. Unknown accounts resolve to 0; duplicate names use the first match (documented).
  • Current account: For the transaction’s own account, users should keep using the balance variable; BALANCE_OF is for other accounts.
  • Implementation notes: Balances are prefetched in existing async paths (runRules, applyActions, schedule template) into _balanceOfPrefetched on the transaction object. Before HyperFormula runs, substituteBalanceOfLiterals replaces each BALANCE_OF("…") with a numeric cent literal so formula execution stays synchronous and we avoid a HyperFormula plugin that needs DB access. getRunningBalanceBeforeTransaction is shared with prepareTransactionForRules for the existing balance field. Split rule actions receive the same prefetch map as the parent row; finalizeTransactionForRules strips internal fields.

Related issue(s)

https://discord.com/channels/937901803608096828/1488212143923921088

Testing

Automated

  • packages/loot-core: balanceOfFormula.test.ts (literal extraction, substitution, id vs name resolution).
  • packages/loot-core: formula-action.test.ts (prefetched map + missing literal → 0).

Manual

  • Create or edit a rule with a formula that uses =BALANCE_OF("Your Account Name") or =BALANCE_OF("<account-id>") and confirm the Set field result matches expectations vs the register for that account.
  • Smoke-test schedule flows that run rule actions if you use rules with formulas there.

Checklist

  • Release notes added (see link above)
  • No obvious regressions in affected areas
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Bundle Stats

Bundle Files count Total bundle size % Changed
desktop-client 27 12.41 MB → 12.41 MB (+1.21 kB) +0.01%
loot-core 1 4.84 MB → 4.84 MB (+3.76 kB) +0.08%
api 1 3.84 MB → 3.84 MB (+4.08 kB) +0.10%
cli 1 7.89 MB 0%
View detailed bundle stats

desktop-client

Total

Files count Total bundle size % Changed
27 12.41 MB → 12.41 MB (+1.21 kB) +0.01%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/rules/customFunctions.ts 📈 +327 B (+30.45%) 1.05 kB → 1.37 kB
src/components/formula/transactionModeFunctions.ts 📈 +404 B (+2.89%) 13.64 kB → 14.04 kB
src/components/sidebar/Account.tsx 📈 +358 B (+2.84%) 12.32 kB → 12.67 kB
src/components/sidebar/Accounts.tsx 📈 +145 B (+1.64%) 8.63 kB → 8.77 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
static/js/index.js 3.31 MB → 3.31 MB (+503 B) +0.01%
static/js/FormulaEditor.js 852.77 kB → 853.16 kB (+404 B) +0.05%
static/js/useTransactionBatchActions.js 4.33 MB → 4.33 MB (+327 B) +0.01%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
static/js/BackgroundImage.js 121.09 kB 0%
static/js/ReportRouter.js 1.17 MB 0%
static/js/TransactionList.js 82.49 kB 0%
static/js/ca.js 189.75 kB 0%
static/js/da.js 104.66 kB 0%
static/js/de.js 174.38 kB 0%
static/js/en-GB.js 8.2 kB 0%
static/js/en.js 175.65 kB 0%
static/js/es.js 181.8 kB 0%
static/js/fr.js 177.08 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.46 kB 0%
static/js/it.js 165.87 kB 0%
static/js/narrow.js 363.02 kB 0%
static/js/nb-NO.js 151.85 kB 0%
static/js/nl.js 108.93 kB 0%
static/js/pl.js 88.34 kB 0%
static/js/pt-BR.js 177.44 kB 0%
static/js/resize-observer.js 18.06 kB 0%
static/js/th.js 179.3 kB 0%
static/js/theme.js 30.79 kB 0%
static/js/uk.js 212.6 kB 0%
static/js/wide.js 295 B 0%
static/js/workbox-window.prod.es5.js 7.33 kB 0%
static/js/zh-Hans.js 94.19 kB 0%

loot-core

Total

Files count Total bundle size % Changed
1 4.84 MB → 4.84 MB (+3.76 kB) +0.08%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/rules/balanceOfFormula.ts 🆕 +965 B 0 B → 965 B
home/runner/work/actual/actual/packages/loot-core/src/server/rules/customFunctions.ts 📈 +337 B (+30.69%) 1.07 kB → 1.4 kB
home/runner/work/actual/actual/packages/loot-core/src/server/transactions/transaction-rules.ts 📈 +1.36 kB (+6.98%) 19.56 kB → 20.92 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/schedule-template.ts 📈 +507 B (+6.95%) 7.13 kB → 7.62 kB
home/runner/work/actual/actual/packages/loot-core/src/server/rules/rule.ts 📈 +137 B (+3.07%) 4.36 kB → 4.5 kB
home/runner/work/actual/actual/packages/loot-core/src/server/sync/index.ts 📈 +350 B (+2.48%) 13.77 kB → 14.12 kB
home/runner/work/actual/actual/packages/loot-core/src/server/rules/action.ts 📈 +154 B (+1.92%) 7.84 kB → 7.99 kB
View detailed bundle breakdown

Added

Asset File Size % Changed
kcab.worker.BgySMm6q.js 0 B → 4.84 MB (+4.84 MB) -

Removed

Asset File Size % Changed
kcab.worker.DOvC4GKD.js 4.84 MB → 0 B (-4.84 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 3.84 MB → 3.84 MB (+4.08 kB) +0.10%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/rules/balanceOfFormula.ts 🆕 +1.23 kB 0 B → 1.23 kB
home/runner/work/actual/actual/packages/loot-core/src/server/rules/customFunctions.ts 📈 +327 B (+30.39%) 1.05 kB → 1.37 kB
home/runner/work/actual/actual/packages/loot-core/src/server/transactions/transaction-rules.ts 📈 +1.44 kB (+7.56%) 19.08 kB → 20.52 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/schedule-template.ts 📈 +496 B (+6.93%) 6.99 kB → 7.47 kB
home/runner/work/actual/actual/packages/loot-core/src/server/rules/rule.ts 📈 +136 B (+3.24%) 4.11 kB → 4.24 kB
home/runner/work/actual/actual/packages/loot-core/src/server/sync/index.ts 📈 +338 B (+2.47%) 13.35 kB → 13.68 kB
home/runner/work/actual/actual/packages/loot-core/src/server/rules/action.ts 📈 +152 B (+2.01%) 7.39 kB → 7.54 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
index.js 3.84 MB → 3.84 MB (+4.08 kB) +0.10%

Smaller
No assets were smaller

Unchanged
No assets were unchanged


cli

Total

Files count Total bundle size % Changed
1 7.89 MB 0%
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
cli.js 7.89 MB 0%

@StephenBrown2 StephenBrown2 requested a review from youngcw as a code owner March 30, 2026 23:57
@actual-github-bot actual-github-bot bot changed the title Add BALANCE_OF() function to fetch balance of an arbitrary account [WIP] Add BALANCE_OF() function to fetch balance of an arbitrary account Mar 30, 2026
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 30, 2026

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit e9686ca
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget/deploys/69d69aac0ab14f0008ef1111
😎 Deploy Preview https://deploy-preview-7335.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
Copy Markdown
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
Copy Markdown

netlify bot commented Mar 30, 2026

Deploy Preview for actualbudget-website ready!

Name Link
🔨 Latest commit e9686ca
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget-website/deploys/69d69aaca899da0008ef5434
😎 Deploy Preview https://deploy-preview-7335.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.

@StephenBrown2 StephenBrown2 changed the title [WIP] Add BALANCE_OF() function to fetch balance of an arbitrary account [WIP] [AI] Add BALANCE_OF() function to fetch balance of an arbitrary account Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Adds a BALANCE_OF(...) rules formula: parsing/decoding of quoted literals, resolving them to account ids, prefetching per-transaction running balances, substituting BALANCE_OF calls with cent literals before formula evaluation, propagating prefetched maps into splits and schedules, plus tests, docs, UI test hooks, and release notes.

Changes

Cohort / File(s) Summary
Formula definition (frontend)
packages/desktop-client/src/components/formula/transactionModeFunctions.ts
Added BALANCE_OF(account_id_or_name: string) entry with localized description.
Documentation & release note
packages/docs/docs/experimental/formulas.md, upcoming-release-notes/7335.md
Documented BALANCE_OF semantics and added release-note entry.
Balance-of utilities & unit tests
packages/loot-core/src/server/rules/balanceOfFormula.ts, packages/loot-core/src/server/rules/balanceOfFormula.test.ts
New utilities: collect formulas from actions, decode quoted literals, extract BALANCE_OF literals, resolve account ids from map or name, and substitute BALANCE_OF literals with cent numeric literals; unit tests for these behaviors.
Rule execution substitution & tests
packages/loot-core/src/server/rules/action.ts, packages/loot-core/src/server/rules/formula-action.test.ts, packages/loot-core/src/server/rules/rule.ts
Substitute BALANCE_OF(...) using transaction._balanceOfPrefetched before HyperFormula evaluation; avoid registering the prefetched map as a named expression; propagate _balanceOfPrefetched into split transactions; added tests for lookup and fallback to 0.
Transaction rules & helpers
packages/loot-core/src/server/transactions/transaction-rules.ts
Added getRunningBalanceBeforeTransaction and prefetchBalanceOfForTransaction; extract BALANCE_OF literals, prefetch per-transaction maps, attach _balanceOfPrefetched, and refactor balance computation to use helpers.
Schedule template integration & tests
packages/loot-core/src/server/budget/schedule-template.ts, packages/loot-core/src/server/budget/schedule-template.test.ts
Schedules now fetch accounts once, build accountsMap, collect formula strings from actions, prefetch BALANCE_OF data and pass it into rule execution via _balanceOfPrefetched; tests stub db.getAccounts.
Sidebar test hooks & e2e updates
packages/desktop-client/src/components/sidebar/Account.tsx, packages/desktop-client/src/components/sidebar/Accounts.tsx, packages/desktop-client/e2e/page-models/account-page.ts, packages/desktop-client/e2e/transactions.test.ts
Added optional balanceTestId prop to Account and passed it for title rows; added page-model locators and updated e2e test to assert sidebar totals unchanged.
Sync spreadsheet aggregates
packages/loot-core/src/server/sync/index.ts
Map __global aggregates to full cell names and recompute specific global aggregate cells after transaction changes; tightened idsPerTable typing.
Misc / test-only tweaks
packages/loot-core/src/server/budget/schedule-template.test.ts
Stubbed db.getAccounts in schedule tests to return an empty array.

Sequence Diagram

sequenceDiagram
    participant Runner as Rule Runner
    participant Collector as Formula Collector
    participant AccountsDB as Accounts DB
    participant Prefetch as Prefetcher
    participant Engine as Formula Engine

    Runner->>Collector: collect BALANCE_OF(...) literals from rule actions
    Collector->>AccountsDB: fetch accounts (build accountsMap)
    Collector->>Prefetch: decode literals, resolve to account IDs using accountsMap
    Prefetch->>AccountsDB: compute running balances for resolved accounts
    Prefetch-->>Collector: return Map<string, cents> (_balanceOfPrefetched)
    Collector-->>Runner: attach _balanceOfPrefetched to transaction(s)

    Runner->>Engine: execute formula (transaction includes _balanceOfPrefetched)
    Engine->>Collector: substitute BALANCE_OF("...") -> cent literals using map
    Collector-->>Engine: substituted formula
    Engine-->>Runner: evaluated result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled strings and unescaped names,
I mapped accounts and fetched their sums,
Substituted calls with tidy cents,
Propagated maps through splits and runs,
Hop — the rules now balance other accounts.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature addition: a BALANCE_OF() function for fetching arbitrary account balances in rule formulas.
Description check ✅ Passed The PR description clearly explains the BALANCE_OF feature, its behavior, resolution logic, use cases, and implementation approach.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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
Copy Markdown
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

🧹 Nitpick comments (1)
packages/loot-core/src/server/transactions/transaction-rules.ts (1)

721-728: Sequential awaits in loop could be parallelized.

The prefetch loop processes transactions sequentially. For batches with many transactions, this could be slow. Consider using Promise.all for parallelization:

♻️ Suggested parallel prefetch
  const formulaStrings = collectFormulasFromActions(parsedActions);
-  for (const trans of transactionsForRules) {
-    trans._balanceOfPrefetched = await prefetchBalanceOfForTransaction(
-      trans,
-      accountsMap,
-      formulaStrings,
-    );
-  }
+  await Promise.all(
+    transactionsForRules.map(async trans => {
+      trans._balanceOfPrefetched = await prefetchBalanceOfForTransaction(
+        trans,
+        accountsMap,
+        formulaStrings,
+      );
+    }),
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/transactions/transaction-rules.ts` around lines
721 - 728, The loop over transactionsForRules awaits
prefetchBalanceOfForTransaction sequentially which is slow; change to run
prefetches in parallel by mapping transactionsForRules to an array of promises
calling prefetchBalanceOfForTransaction(trans, accountsMap, formulaStrings),
await Promise.all on that array, then assign each resolved result back to the
corresponding trans._balanceOfPrefetched (or keep the same order by mapping
index -> trans); keep references to collectFormulasFromActions,
transactionsForRules, prefetchBalanceOfForTransaction and the target property
_balanceOfPrefetched when implementing the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/loot-core/src/server/budget/schedule-template.ts`:
- Around line 79-96: scheduleRuleContext is missing a date so
prefetchBalanceOfForTransaction (and ultimately
getRunningBalanceBeforeTransaction / BALANCE_OF) falls back to currentDay(); set
the scheduled transaction date when building scheduleRuleContext (use the
schedule's effective date like next_date_string or equivalent) and also populate
id/sort_order placeholders (null/0) so downstream code treats it as a future
transaction; update the object created for scheduleRuleContext before calling
prefetchBalanceOfForTransaction and rule.execActions to ensure BALANCE_OF
queries use the scheduled date.

---

Nitpick comments:
In `@packages/loot-core/src/server/transactions/transaction-rules.ts`:
- Around line 721-728: The loop over transactionsForRules awaits
prefetchBalanceOfForTransaction sequentially which is slow; change to run
prefetches in parallel by mapping transactionsForRules to an array of promises
calling prefetchBalanceOfForTransaction(trans, accountsMap, formulaStrings),
await Promise.all on that array, then assign each resolved result back to the
corresponding trans._balanceOfPrefetched (or keep the same order by mapping
index -> trans); keep references to collectFormulasFromActions,
transactionsForRules, prefetchBalanceOfForTransaction and the target property
_balanceOfPrefetched when implementing the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c9366f52-fdf2-43a5-b20c-29ab51557fdc

📥 Commits

Reviewing files that changed from the base of the PR and between 3b14fd0 and 7de658f.

📒 Files selected for processing (10)
  • packages/desktop-client/src/components/formula/transactionModeFunctions.ts
  • packages/docs/docs/experimental/formulas.md
  • packages/loot-core/src/server/budget/schedule-template.ts
  • packages/loot-core/src/server/rules/action.ts
  • packages/loot-core/src/server/rules/balanceOfFormula.test.ts
  • packages/loot-core/src/server/rules/balanceOfFormula.ts
  • packages/loot-core/src/server/rules/formula-action.test.ts
  • packages/loot-core/src/server/rules/rule.ts
  • packages/loot-core/src/server/transactions/transaction-rules.ts
  • upcoming-release-notes/7335.md

@github-actions

This comment has been minimized.

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

/update-vrt

There should be no reason that the UI changed, but apparently 134 pixels are different, so here goes.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (3)
packages/loot-core/src/server/budget/schedule-template.ts (1)

84-85: Move db.getAccounts() out of the template loop.

This now runs once per schedule template, so a month with many schedules pays for the same accounts query and map construction N times before any rule work. Fetching once per createScheduleList call keeps the BALANCE_OF prefetch off this hot I/O path.

Suggested refactor
 async function createScheduleList(
   templates: ScheduleTemplate[],
   current_month: string,
   category: CategoryEntity,
   currency: Currency,
 ) {
   const t: Array<ScheduleTemplateTarget> = [];
   const errors: string[] = [];
+  const accounts = (await db.getAccounts()) ?? [];
+  const accountsMap = new Map(accounts.map(account => [account.id, account]));

   for (const template of templates) {
     // ...
-    const accounts = (await db.getAccounts()) ?? [];
-    const accountsMap = new Map(accounts.map(a => [a.id, a]));
     const scheduleRuleContext: TransactionEntity = {
       amount: scheduleAmount,
       category: category.id,
       subtransactions: [],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/budget/schedule-template.ts` around lines 84 -
85, The accounts lookup and map construction (db.getAccounts() and accountsMap)
should be moved out of the per-template loop in createScheduleList so we only
fetch once per createScheduleList invocation; remove the const accounts = (await
db.getAccounts()) ?? []; and const accountsMap = new Map(...) from inside the
schedule template loop and instead call db.getAccounts() once at the start of
createScheduleList, build accountsMap there, and reuse that accountsMap when
evaluating each schedule template (where BALANCE_OF currently triggers the
lookup).
packages/loot-core/src/server/budget/schedule-template.test.ts (1)

21-24: Add one assertion for the schedule-specific BALANCE_OF path.

This mock keeps the suite green, but it still doesn't verify the new behavior in createScheduleList: building a scheduled transaction context and passing _balanceOfPrefetched into rule.execActions. One focused case here would lock down the schedule flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/budget/schedule-template.test.ts` around lines
21 - 24, Add a focused test assertion in the schedule-template.test to verify
schedule-specific BALANCE_OF behavior: after calling createScheduleList, assert
that the mocked rule.execActions (the rule passed into createScheduleList) was
invoked with a scheduled transaction context that includes the
_balanceOfPrefetched property set to the expected value for the BALANCE_OF path.
Locate the test setup that mocks db.getAccounts and the rule object, call
createScheduleList with a schedule that triggers the BALANCE_OF branch, and add
an assertion that rule.execActions was called and that one of its call arguments
(the context) contains _balanceOfPrefetched with the prefetched balance data.
packages/loot-core/src/server/transactions/transaction-rules.ts (1)

1020-1032: Cache by resolved account id, not just literal text.

If rules mix BALANCE_OF("<id>") and BALANCE_OF("Account Name") for the same account, this loop queries the same running balance twice because deduping happens before resolution. Cache on accountId to keep semantics identical and avoid extra AQL work.

Suggested refactor
 export async function prefetchBalanceOfForTransaction(
   trans: TransactionEntity,
   accountsMap: Map<string, db.DbAccount>,
   formulas: string[],
 ): Promise<Map<string, number>> {
   const map = new Map<string, number>();
   const literals = new Set<string>();
+  const balancesByAccountId = new Map<string, number>();

   for (const f of formulas) {
     for (const lit of extractBalanceOfLiterals(f)) {
       literals.add(lit);
     }
   }

   for (const literal of literals) {
     const accountId = resolveAccountIdForBalanceOf(literal, accountsMap);
     if (accountId) {
-      map.set(
-        literal,
-        await getRunningBalanceBeforeTransaction(trans, accountId),
-      );
+      if (!balancesByAccountId.has(accountId)) {
+        balancesByAccountId.set(
+          accountId,
+          await getRunningBalanceBeforeTransaction(trans, accountId),
+        );
+      }
+      map.set(literal, balancesByAccountId.get(accountId) ?? 0);
     } else {
       map.set(literal, 0);
     }
   }
   return map;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/transactions/transaction-rules.ts` around lines
1020 - 1032, The loop currently deduplicates BALANCE_OF(...) by literal text
which causes duplicate AQL queries when different literals resolve to the same
account; change it to dedupe by resolved account id: iterate the literals from
extractBalanceOfLiterals(formulas), call resolveAccountIdForBalanceOf(literal,
accountsMap) for each, and maintain a small cache keyed by accountId (e.g.,
balanceCache) so you only call getRunningBalanceBeforeTransaction(trans,
accountId) once per accountId, then set map.set(literal, cachedBalance) for
every literal that resolves to that accountId; keep references to
extractBalanceOfLiterals, resolveAccountIdForBalanceOf,
getRunningBalanceBeforeTransaction, map, accountsMap and trans to locate the
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/loot-core/src/server/budget/schedule-template.test.ts`:
- Around line 21-24: Add a focused test assertion in the schedule-template.test
to verify schedule-specific BALANCE_OF behavior: after calling
createScheduleList, assert that the mocked rule.execActions (the rule passed
into createScheduleList) was invoked with a scheduled transaction context that
includes the _balanceOfPrefetched property set to the expected value for the
BALANCE_OF path. Locate the test setup that mocks db.getAccounts and the rule
object, call createScheduleList with a schedule that triggers the BALANCE_OF
branch, and add an assertion that rule.execActions was called and that one of
its call arguments (the context) contains _balanceOfPrefetched with the
prefetched balance data.

In `@packages/loot-core/src/server/budget/schedule-template.ts`:
- Around line 84-85: The accounts lookup and map construction (db.getAccounts()
and accountsMap) should be moved out of the per-template loop in
createScheduleList so we only fetch once per createScheduleList invocation;
remove the const accounts = (await db.getAccounts()) ?? []; and const
accountsMap = new Map(...) from inside the schedule template loop and instead
call db.getAccounts() once at the start of createScheduleList, build accountsMap
there, and reuse that accountsMap when evaluating each schedule template (where
BALANCE_OF currently triggers the lookup).

In `@packages/loot-core/src/server/transactions/transaction-rules.ts`:
- Around line 1020-1032: The loop currently deduplicates BALANCE_OF(...) by
literal text which causes duplicate AQL queries when different literals resolve
to the same account; change it to dedupe by resolved account id: iterate the
literals from extractBalanceOfLiterals(formulas), call
resolveAccountIdForBalanceOf(literal, accountsMap) for each, and maintain a
small cache keyed by accountId (e.g., balanceCache) so you only call
getRunningBalanceBeforeTransaction(trans, accountId) once per accountId, then
set map.set(literal, cachedBalance) for every literal that resolves to that
accountId; keep references to extractBalanceOfLiterals,
resolveAccountIdForBalanceOf, getRunningBalanceBeforeTransaction, map,
accountsMap and trans to locate the changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d122df3f-adbe-4d6e-b3a3-67f0650397cd

📥 Commits

Reviewing files that changed from the base of the PR and between 7de658f and e808724.

📒 Files selected for processing (11)
  • packages/desktop-client/src/components/formula/transactionModeFunctions.ts
  • packages/docs/docs/experimental/formulas.md
  • packages/loot-core/src/server/budget/schedule-template.test.ts
  • packages/loot-core/src/server/budget/schedule-template.ts
  • packages/loot-core/src/server/rules/action.ts
  • packages/loot-core/src/server/rules/balanceOfFormula.test.ts
  • packages/loot-core/src/server/rules/balanceOfFormula.ts
  • packages/loot-core/src/server/rules/formula-action.test.ts
  • packages/loot-core/src/server/rules/rule.ts
  • packages/loot-core/src/server/transactions/transaction-rules.ts
  • upcoming-release-notes/7335.md
✅ Files skipped from review due to trivial changes (4)
  • upcoming-release-notes/7335.md
  • packages/docs/docs/experimental/formulas.md
  • packages/loot-core/src/server/rules/rule.ts
  • packages/loot-core/src/server/rules/balanceOfFormula.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/loot-core/src/server/rules/formula-action.test.ts
  • packages/loot-core/src/server/rules/balanceOfFormula.ts
  • packages/loot-core/src/server/rules/action.ts

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 31, 2026
Auto-generated by VRT workflow

PR: actualbudget#7335
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 31, 2026
@StephenBrown2 StephenBrown2 changed the title [WIP] [AI] Add BALANCE_OF() function to fetch balance of an arbitrary account [AI] Add BALANCE_OF() function to fetch balance of an arbitrary account Mar 31, 2026
@YEGCSharpDev
Copy link
Copy Markdown

Tested on a schedule linked formula. Details in the linked discord thread.

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 31, 2026
Auto-generated by VRT workflow

PR: actualbudget#7335
@StephenBrown2
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

✅ Actions performed

Reviews resumed.

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 31, 2026
@youngcw
Copy link
Copy Markdown
Member

youngcw commented Apr 2, 2026

Seems to function correctly. I still need to look over the code more

@matt-fidd matt-fidd force-pushed the master branch 2 times, most recently from 5c7c70d to d262f7d Compare April 5, 2026 17:13
@youngcw
Copy link
Copy Markdown
Member

youngcw commented Apr 6, 2026

Have you looked at all into making a custom function instead of replacing the function in the formula string?

licenseKey: 'gpl-v3',
localeLang: typeof locale === 'string' ? locale : 'en-US',
language: 'enUS',
localeLang: typeof locale === 'string' ? locale : 'en-US',
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reordering to match the other instances of this call, no other reason.

CustomFunctionsPlugin.implementedFunctions = {
INTEGER_TO_AMOUNT: {
method: 'integerToAmount',
BALANCE_OF: {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Alphabetical reordering.

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

Have you looked at all into making a custom function instead of replacing the function in the formula string?

It seems like that should be possible, but I gave it a try and it doesn't appear to be working for me in the deploy preview. Can you offer any insights that I might have missed?

@youngcw
Copy link
Copy Markdown
Member

youngcw commented Apr 6, 2026

The only thing I can think of on why it wouldn't be working is that there are 3 separate instances of hyperformula and you need to add the functions to the right one.

I like the idea if having custom functions instead of all the overhead of precalculation and pre editing the formulas.

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

StephenBrown2 commented Apr 7, 2026

I spoke too soon. I was able to get it to work as it is now, I must have just been looking in the wrong spot. I'll see if I can remove the prefetching next, probably something like the BUDGET_QUERY implementations.

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

So, in useFormulaExecution, BUDGET_QUERY never runs as a real HyperFormula function. The hook walks the string, does the async work (fetchBudgetDimensionValueDirect et al.), then replaces the whole BUDGET_QUERY(...) chunk with a numeric literal. HyperFormula only ever sees the cleaned-up formula.

BALANCE_OF is still the same idea (resolve external data before the engine evaluates), but the implementation is different: prefetch balances into a map and pass it on buildEmpty({ context }), and the custom function just does a lookup. It used to do straight string substitution for BALANCE_OF just like BUDGET_QUERY; I moved to a proper HF function as requested.

Either way, since executeFormulaSync is used, the async / AQL stuff can’t live inside the HyperFormula callback (synchronous Action.exec and forEach rule runner). The interpreter expects a synchronous return. So the pattern matches BUDGET_QUERY: async orchestration first, then HF.

I could work on making all formula executions async, so I could put AQL directly into the custom function, but that seems like more work than needed. I may be wrong though.

@youngcw
Copy link
Copy Markdown
Member

youngcw commented Apr 7, 2026

No async is a pain. Ultimately I would like all the hyperformula stuff to be as self contained as possible. Im annoyed that there are 3 separate instantiations of it instead of one shared source of settings/functions/pre-parsers. That should get cleaned up eventually.

Doing all the prep only just before HF is called is a good fallback. According to what I'm seeing online, thats how it is generally done. Would that remove the need to do the prefecthing in places like templates?

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

Based on my research, assisted by Cursor:

Doing all prep immediately before HF is the usual pattern and can remove the need for separate prefetch blocks in templates and runRules, if that prep lives in the single async-capable path that always runs before evaluation and you thread await through rule execution. However, it appears that without going async (or without passing prefetched data in), one can't actually "just prep" with AQL inside the current sync Action.exec chain.

The prefetch work (async balance queries) has to happen somewhere before executeFormulaSync as long as rules stay sync and HF stays sync-only inside the plugin, so I could extract it to a helper that's called and pull the same three steps into one small shared helper in loot-core (e.g. next to prefetchBalanceOfForTransaction / collectFormulasFromActions):

  1. collectFormulasFromActions(actions)
  2. await prefetchBalanceOfForTransaction(trans, accountsMap, formulaStrings)
  3. return { ...trans, _balanceOfPrefetched } (or mutate and return)

Then schedule-template becomes a single await of that helper before rule.execActions. runRules could use the same helper for the "attach prefetch to this transaction for these actions" part, even though runRules still gathers formulas across many rules (slightly different inputs, same primitive).

What do you think of that? I could see if I could consolidate the HF stuff into a single module separately, but it sounds like async is still the way if we want to to be as streamlined as possible.

@youngcw
Copy link
Copy Markdown
Member

youngcw commented Apr 8, 2026

I guess for now add any updates you have and fix the conflicts. It probably will be best to clean up the HF code in a separate PR.

@StephenBrown2
Copy link
Copy Markdown
Contributor Author

Conflicts fixed. Agreed about consolidating HF in a separate PR, that was my intention for "separately".

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.

3 participants