actualbudget/actual · error
Income category "${template.category}" not found for percent
Error message
Income category "${template.category}" not found for percentage template What it means
The `percentage` budget template distributes a percentage of an income category's monthly income into a category. This error is thrown when the category named/id given in the template does not match any existing income category (is_income) in the budget. The lookup matches by id or case-insensitive name, so a mismatch means the category doesn't exist or isn't marked as income.
Source
Thrown at packages/loot-core/src/server/budget/category-template-context.ts:858
);
} else {
sheetName = monthUtils.sheetForMonth(templateContext.month);
}
if (cat === 'all income') {
monthlyIncome = await getSheetValue(sheetName, `total-income`);
} else if (cat === 'available funds') {
monthlyIncome = availableFunds;
} else {
// Text templates address income categories by name (e.g. `#template
// 10% of Salary`); the UI's CategoryAutocomplete stores the category
// id. Accept either form.
const incomeCat = (await db.getCategories()).find(
c =>
c.is_income &&
(c.id === template.category || c.name.toLocaleLowerCase() === cat),
);
if (!incomeCat) {
throw new Error(
`Income category "${template.category}" not found for percentage template`,
);
}
monthlyIncome = await getSheetValue(
sheetName,
`sum-amount-${incomeCat.id}`,
);
}
return Math.max(0, Math.round(monthlyIncome * (percent / 100)));
}
static async runAverage(
template: AverageTemplate,
templateContext: CategoryTemplateContext,
): Promise<number> {
let average = await getCategoryAverage({
month: templateContext.month,View on GitHub (pinned to d4334cb6e6)
Solutions
- Verify the category name in the template exactly matches an existing income category (matching is case-insensitive by name or by category id).
- Mark the intended category as income ('Income' type in category group settings) if it should be usable here.
- Fix or remove the stale `#template ... % of <category>` line in the category note.
- Query the budget (API/db) to list income categories and use one of those exact names.
Example fix
// before #template 10% of Paycheck // Paycheck is an expense category // after (Paycheck marked as income, or use an income category) #template 10% of Income
Defensive patterns
Strategy: validation
Validate before calling
const cats = await db.getCategories();
const incomeCat = cats.find(
c => c.is_income && c.name.toLocaleLowerCase() === template.category.toLocaleLowerCase(),
);
if (!incomeCat) {
const names = cats.filter(c => c.is_income).map(c => c.name);
throw new Error(`'${template.category}' is not an income category. Available: ${names.join(', ')}`);
} Type guard
function isIncomeCategory(
cats: { id: string; name: string; is_income: boolean }[],
name: string,
): boolean {
return cats.some(c => c.is_income && c.name.toLocaleLowerCase() === name.toLocaleLowerCase());
} Try / catch
try {
await applyTemplates(categoryIds, month);
} catch (e) {
if (e instanceof Error && e.message.includes('not found for percentage template')) {
logger.warn('Percentage template references missing income category; skipping', { error: e.message });
} else {
throw e;
}
} Prevention
- Reference income categories by their exact current name in templates
- Re-check template notes after renaming or deleting categories
- Ensure the target category is marked as income before using it in a % template
- Use category ids instead of names when building templates programmatically
When it happens
Trigger: A template line like `#template 10% of Income` where `Income` is not an existing category name (typo, renamed category, deleted category, or the category exists but is not flagged as income).
Common situations: Category renamed or deleted after the template was written, using an on-budget expense category instead of an income category, restoring a budget where income categories were re-created with new ids, or case/punctuation differences in the name.
Related errors
- Unrecognized periodic period: ${String(period)}
- 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)
- No update fields provided. Use --name or --hidden.
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/c11b5cc3991887cd.
Report an issue: GitHub.