actualbudget/actual · error
Unknown cleanup row type: ${String(row)}
Error message
Unknown cleanup row type: ${String(row)} What it means
toCleanupTemplate dispatches on a discriminated union of cleanup row types; the default branch throws when a row has an unrecognized `type`. This means the row data doesn't conform to the known cleanup row schema (group, overspend, etc.) and cannot be converted into a cleanup template. It is a defensive exhaustiveness check against malformed or newer-unknown row payloads.
Source
Thrown at packages/loot-core/src/server/budget/cleanup-template-notes.ts:110
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(
names: ReadonlySet<string>,
): Promise<Map<string, string>> {
const map = new Map<string, string>();
for (const name of names) {
const id = await resolveCleanupGroup(name);
map.set(name.toLowerCase(), id);
}View on GitHub (pinned to d4334cb6e6)
Solutions
- Inspect the offending row's type value and correct it to a supported cleanup row type.
- Update Actual to a version whose toCleanupTemplate handles the row type being used.
- Fix the parser/builder that produced the row so it only emits known types.
- If persisting templates, clear/repair the stale stored rows.
Example fix
// before
{ type: 'overspendd', group: 'Groceries' }
// after
{ type: 'overspend', group: 'Groceries' } Defensive patterns
Strategy: type-guard
Validate before calling
const KNOWN_TYPES = new Set(['group', 'overspend']);
const unknown = rows.filter(r => !KNOWN_TYPES.has(r.type));
if (unknown.length > 0) {
throw new Error(`Unknown cleanup row types: ${unknown.map(r => r.type).join(', ')}`);
} Type guard
type GroupRow = { type: 'group'; name: string; weight?: number };
type OverspendRow = { type: 'overspend'; group: string; weight?: number };
function isCleanupRow(r: unknown): r is GroupRow | OverspendRow {
return (
typeof r === 'object' && r !== null &&
'type' in r &&
((r as GroupRow).type === 'group' || (r as OverspendRow).type === 'overspend')
);
} Try / catch
try {
const cleanup = toCleanupTemplate(rows);
} catch (e) {
if (e instanceof Error && e.message.startsWith('Unknown cleanup row type')) {
logger.warn('Skipping cleanup template with unknown row type', { error: e.message });
} else {
throw e;
}
} Prevention
- Validate row objects against the cleanup row union type before conversion
- Keep note parsers and template converters on the same version
- Use a discriminated union with exhaustive switch (never silenty default) in your own code
- Sanitize/repair persisted template data after upgrades
When it happens
Trigger: Passing a row whose `type`/discriminator is misspelled, undefined, or produced by a newer version of the note parser than the converter understands; e.g. `#cleanup prioritize ...` parsed into a row type not handled by this switch.
Common situations: Version mismatch between the code that parses cleanup notes and the code that converts them, hand-built row objects with typos in the type field, or corrupted/stale persisted template data.
Related errors
- Cleanup group name cannot be empty
- Unresolved cleanup group for overspend row: ${row.group}
- 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/2ecca31699a56652.
Report an issue: GitHub.