actualbudget/actual · error

Cleanup group name cannot be empty

Error message

Cleanup group name cannot be empty

What it means

resolveCleanupGroup normalizes a cleanup-group name for budget cleanup templates and rejects names that are empty after trimming. Cleanup groups must have a non-empty, unique (case-insensitive) name so rows can reference them. This guard prevents creating or resolving an unnamed group in the cleanup_groups table.

Source

Thrown at packages/loot-core/src/server/budget/cleanup-groups.ts:8

import { v4 as uuidv4 } from 'uuid';

import * as db from '#server/db';

export async function resolveCleanupGroup(name: string): Promise<string> {
  const trimmed = name.trim();
  if (trimmed.length === 0) {
    throw new Error('Cleanup group name cannot be empty');
  }
  const key = trimmed.toLowerCase();
  const existing = await db.first<{ id: string; tombstone: 0 | 1 }>(
    `SELECT id, tombstone FROM cleanup_groups WHERE lower(name) = ? LIMIT 1`,
    [key],
  );
  if (existing) {
    if (existing.tombstone) {
      await db.update('cleanup_groups', { id: existing.id, tombstone: 0 });
    }
    return existing.id;
  }
  const id = uuidv4();
  await db.insertWithSchema('cleanup_groups', {
    id,
    name: trimmed,
    tombstone: 0,
  });

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Provide a non-empty group name, e.g. resolveCleanupGroup('Groceries cap').
  2. Fix the template line to include the group name after the keyword.
  3. Trim/validate user input before calling; reject empty names in your UI or script.
  4. If the name comes from parsing, check the split logic isn't dropping the value.

Example fix

// before
await resolveCleanupGroup(groupNameFromNote); // '' when note is '#cleanup group:'
// after
if (!groupNameFromNote?.trim()) throw new Error('Cleanup group name required');
await resolveCleanupGroup(groupNameFromNote);
Defensive patterns

Strategy: validation

Validate before calling

function isValidGroupName(name: unknown): name is string {
  return typeof name === 'string' && name.trim().length > 0;
}
if (!isValidGroupName(rawName)) {
  throw new Error('A non-empty cleanup group name is required');
}
await resolveCleanupGroup(rawName);

Type guard

function isNonEmptyString(v: unknown): v is string {
  return typeof v === 'string' && v.trim().length > 0;
}

Try / catch

try {
  const groupId = await resolveCleanupGroup(name);
} catch (e) {
  if (e instanceof Error && e.message === 'Cleanup group name cannot be empty') {
    logger.warn('Skipping cleanup rule with empty group name');
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling resolveCleanupGroup('') or resolveCleanupGroup(' '), or a template/automation path that derives the group name from user input that ends up empty (e.g. `#cleanup group:` with nothing after the colon).

Common situations: Malformed cleanup template note with a missing group name, a form field left blank in tooling that calls the API, or string splitting that yields an empty token.

Related errors


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