actualbudget/actual · error
Unresolved cleanup group for overspend row: ${row.group}
Error message
Unresolved cleanup group for overspend row: ${row.group} What it means
When converting a cleanup template 'overspend' row into its internal form, the row's group name must resolve to an existing cleanup group via the case-insensitive name→id map. If the group referenced by the overspend row was never defined (or was defined with a different name/deleted), this error is thrown. It indicates a dangling reference between cleanup rows and cleanup group definitions.
Source
Thrown at packages/loot-core/src/server/budget/cleanup-template-notes.ts:103
}
function toCleanupTemplate(
row: ParsedCleanupRow,
nameToId: Map<string, string>,
): CleanupTemplate {
switch (row.type) {
case 'source':
return { role: 'source', groupId: resolveGroup(row.group, nameToId) };
case 'sink':
return {
role: 'sink',
groupId: resolveGroup(row.group, nameToId),
weight: row.weight,
};
case 'overspend': {
const groupId = nameToId.get(row.group.toLowerCase());
if (groupId == null) {
throw new Error(
`Unresolved cleanup group for overspend row: ${row.group}`,
);
}
return { role: 'overspend', groupId };
}
default:
throw new Error(`Unknown cleanup row type: ${String(row)}`);
}
}
function resolveGroup(
name: string | null,
nameToId: Map<string, string>,
): string | null {
return name != null ? (nameToId.get(name.toLowerCase()) ?? null) : null;
}
async function resolveCleanupGroups(View on GitHub (pinned to d4334cb6e6)
Solutions
- Add or restore the cleanup group definition whose name matches the overspend row's group.
- Fix the overspend row's group name to exactly match an existing group (case-insensitive, trimmed).
- Remove the orphaned overspend row if the group is intentionally gone.
- List existing cleanup groups (cleanup_groups table / API) and align names before applying the template.
Example fix
// before #cleanup overspend Groceries-caps // group 'Groceries-caps' undefined // after (group defined first) #cleanup group Groceries #cleanup overspend Groceries
Defensive patterns
Strategy: validation
Validate before calling
const groups = new Set(rows.filter(r => r.type === 'group').map(r => r.name.toLowerCase()));
for (const row of rows.filter(r => r.type === 'overspend')) {
if (!groups.has(row.group.toLowerCase())) {
throw new Error(`overspend row references undefined group: ${row.group}`);
}
}
const template = toCleanupTemplate(rows); Type guard
function isKnownGroup(row: { group: string }, nameToId: Map<string, string>): boolean {
return nameToId.has(row.group.toLowerCase());
} Try / catch
try {
const cleanup = toCleanupTemplate(rows);
} catch (e) {
if (e instanceof Error && e.message.startsWith('Unresolved cleanup group for overspend row')) {
logger.warn('Dropping overspend row with unresolved group', { error: e.message });
} else {
throw e;
}
} Prevention
- Define every cleanup group before any overspend row that references it
- Update all references together when renaming a group
- Delete orphaned overspend rows when removing a group
- Cross-check group names in template notes with a validation script before applying
When it happens
Trigger: A cleanup template contains an `overspend` row referencing `row.group` but no matching `group` definition row exists in nameToId — e.g. the group definition line was removed, renamed, or the overspend row has a typo in the group name.
Common situations: Editing budget template notes and renaming a group without updating the overspend rows that reference it, deleting a cleanup group while overspend rules still point at it, or case/whitespace mismatches (lookup lowercases row.group).
Related errors
- Cleanup group name cannot be empty
- Unknown cleanup row type: ${String(row)}
- Invalid --name: must be a non-empty string.
- No update fields provided. Use --name or --offbudget.
- Invalid cutoff date: expected a valid date (e.g. YYYY-MM-DD)
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/99d9090d387a4e92.
Report an issue: GitHub.