santifer/career-ops · error
Template missing required placeholders: }}`).join(', ')}
Error message
Template ${where} missing required placeholders: ${v.missing.map((m) => `{{${m}}}`).join(', ')} What it means
resolveTemplate validates that HTML CV templates contain all required placeholders (e.g. {{name}}, {{experience}}) before returning the template path. If a template file passes initial resolution but lacks any placeholder the CV generator will substitute, validation fails and this error is thrown, naming the actual file (pack-qualified when the template comes from a pack). It exists to fail fast at template-resolution time instead of producing a silently broken CV with unsubstituted tokens.
Solutions
- Open the file named in the error message and re-add the listed {{placeholder}} tokens exactly as shown (they are rendered verbatim in the message).
- If you don't need a custom design, restore the stock templates/cv-template.html (git checkout templates/cv-template.html or re-copy from templates/).
- If the template comes from a pack, update the pack or fix the pack-qualified file `<pack>/<fileFor(chosen)>` named in the error.
- If the placeholder list itself is wrong for your use case, update the required-placeholder definition in validateTemplate rather than the template.
- As a last resort, set format to 'latex'/'tex' path if your template is a .tex file, since validation only runs for format === 'html'.
Example fix
// before: template missing the contact block after a redesign edit
<div class="experience">{{experience}}</div>
// after: restore the required placeholders the validator expects
<div class="header">
<h1>{{name}}</h1>
<p>{{contact}}</p>
</div>
<div class="experience">{{experience}}</div> Defensive patterns
Strategy: validation
Validate before calling
import { validateTemplate } from './cv-templates.mjs';
const v = validateTemplate(templatePath, kind);
if (!v.ok) {
console.error(`Fix placeholders before generating: ${v.missing.map(m => `{{${m}}}`).join(', ')}`);
process.exit(1);
} Type guard
function templateHasPlaceholders(html, required) {
return required.every(p => html.includes(`{{${p}}}`));
} Try / catch
try {
const path = await resolveTemplate({ format: 'html', kind });
} catch (err) {
if (err.message.includes('missing required placeholders')) {
console.error('Template is missing tokens; restore stock template or re-add:', err.message);
} else throw err;
} Prevention
- Run validateTemplate (or a placeholder grep) as a pre-commit hook on any edits to templates/*.html
- Keep pack templates in sync with the placeholder contract after generator updates
- Diff custom templates against the stock template after each career-ops update
- Never delete a section outright — replace its contents while keeping the {{placeholder}} token
When it happens
Trigger: Calling resolveTemplate with format === 'html' (directly or via CV generation) where validateTemplate(path, kind) returns {ok:false, missing:[...]} for the chosen template — i.e. a hand-edited, custom, or pack-shipped HTML template that dropped one or more {{placeholder}} tokens, or renamed one to something the generator does not recognize.
Common situations: Users customize templates/cv-template.html and accidentally delete a section wrapper containing a placeholder; a third-party CV pack's HTML file was edited or is from an older template contract version; a template was converted from the LaTeX variant (different placeholder names) and used as HTML; a rename inside the template didn't match the generator's expected placeholder set.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- apify: entry has invalid field_map. Each of title, url…
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
- arbeitnow: invalid URL
- ashby: invalid URL
- bamboohr: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/dae413dd6f2d6547.
Report an issue: GitHub.
Appendix: source
Thrown at cv-templates.mjs:272
// Every by-name caller lands here — build-cv-latex.mjs, generate-cover-letter.mjs.
const found = discover(kind, { dir, format });
let entry = found.get(chosen);
if (!entry && fallback && chosen !== 'standard') {
chosen = 'standard';
entry = found.get(chosen);
}
if (!entry) {
throw new Error(`Template not found for kind=${kind} name=${chosen} (${fileFor(chosen)})`);
}
const path = entry.path;
if (format === 'html') {
const v = validateTemplate(path, kind);
if (!v.ok) {
// Name the file that is actually short, not the flat filename it would
// have had. For a pack these differ, and the flat name points at nothing.
const where = entry.pack ? `${entry.pack}/${fileFor(chosen)}` : fileFor(chosen);
throw new Error(
`Template ${where} missing required placeholders: ${v.missing.map((m) => `{{${m}}}`).join(', ')}`
);
}
}
return path;
}
// ---- CLI ----
const isMain = isMainModule(import.meta.url);
if (isMain) {
const argv = process.argv.slice(2);
const cmd = argv[0];
const kind = argv[1];
const flags = Object.fromEntries(
argv.filter((a) => a.startsWith('--')).map((a) => {
const [k, v] = a.replace(/^--/, '').split('=');
return [k, v ?? true];
})View on GitHub (pinned to aac998c7ed)