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

  1. Add or restore the cleanup group definition whose name matches the overspend row's group.
  2. Fix the overspend row's group name to exactly match an existing group (case-insensitive, trimmed).
  3. Remove the orphaned overspend row if the group is intentionally gone.
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/99d9090d387a4e92. Report an issue: GitHub.