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

  1. Inspect the offending row's type value and correct it to a supported cleanup row type.
  2. Update Actual to a version whose toCleanupTemplate handles the row type being used.
  3. Fix the parser/builder that produced the row so it only emits known types.
  4. 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

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


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