santifer/career-ops · error
Unresolved placeholders
Error message
Unresolved placeholders: ${[...unresolved].join(', ')} What it means
generate-cover-letter.mjs's buildHtml renders a cover-letter HTML template by single-pass substituting a fixed map of {{TOKEN}} placeholders. If the template contains an all-caps {{TOKEN}} that has no entry in the replacement map (e.g. a typo'd or unsupported token in a custom template), it is left literal, collected, and the function throws listing every unresolved token — refusing to ship a letter containing a literal {{TOKEN}}.
Solutions
- Read the listed token names from the error and fix the template: either correct the typo to a supported token or remove the unsupported token
- Check which tokens the renderer supports (NAME, CONTACT_LINE, CREDENTIALS_BLOCK, ROLE_TITLE, DATELINE, GREETING_BLOCK, OPENING, PROFILE_INTRO, ACHIEVEMENTS_BLOCK, PROBLEMS_BLOCK, CLOSING_BLOCK, LANGUAGE_CLOSING_BLOCK, SIGNATURE_BLOCK, FOOTNOTES_BLOCK) and use only those
- If the token is intentionally literal text in the letter, rewrite it so it does not match /\{\{[A-Z_]+\}\}/ (e.g. lowercase or extra spacing)
- Re-run the generator after editing the template
Example fix
<!-- before -->
Dear {{HIRING_MANAGER}},
<!-- after -->
{{GREETING_BLOCK}}
<!-- or, for a literal double-brace example in text: -->
<!-- before --> use {{TOKEN}} syntax
<!-- after --> use {{TOKEN}} syntax Defensive patterns
Strategy: validation
Validate before calling
const KNOWN = ['NAME','CONTACT_LINE','CREDENTIALS_BLOCK','ROLE_TITLE','DATELINE','GREETING_BLOCK','OPENING','PROFILE_INTRO','ACHIEVEMENTS_BLOCK','PROBLEMS_BLOCK','CLOSING_BLOCK','LANGUAGE_CLOSING_BLOCK','SIGNATURE_BLOCK','FOOTNOTES_BLOCK'];
const found = [...template.matchAll(/\{\{([A-Z_]+)\}\}/g)].map(m => m[1]);
const bad = found.filter(t => !KNOWN.includes(t));
if (bad.length) throw new Error(`Unsupported placeholders: ${bad.join(', ')}`); Type guard
const isSupportedToken = (token) => typeof token === 'string' && Object.prototype.hasOwnProperty.call(replacements, token);
Try / catch
try {
const html = buildHtml(payload);
} catch (err) {
if (err.message.startsWith('Unresolved placeholders:')) {
console.error('Fix these template tokens:', err.message.slice('Unresolved placeholders:'.length));
process.exit(1);
}
throw err;
} Prevention
- Only use tokens from the documented replacement map when editing cover-letter templates
- Lint templates against the token list in CI before running the generator
- Never write literal {{UPPERCASE}} text in letter content; escape or reformat it
- After a career-ops update, diff your custom templates against the shipped ones for new/renamed tokens
When it happens
Trigger: Calling buildHtml (or the CLI with --format cover) against a cover-letter template that contains a {{TOKEN}} not in the replacements map: a typo like {{NAMEE}} or {{COMPANY}}, a hand-edited custom cover template in cv-templates.mjs with a token the renderer never fills, or an upstream template update adding a token before the renderer gained a replacement for it.
Common situations: Users customizing templates/cv-template variants for cover letters add a new section with a placeholder they invent; template and renderer versions drift after a career-ops update; a fat-fingered token name during template editing.
Related errors
- Template missing required placeholders: }}`).join(', ')}
- Unknown template format
- 4dayweek: invalid URL
- 4dayweek: URL must use HTTPS
- a 40-hex commit --sha is required
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/560b72bfef253e99.
Report an issue: GitHub.
Appendix: source
Thrown at generate-cover-letter.mjs:264
// a custom cover-letter template (KINDS.cover in cv-templates.mjs) carrying a
// typo'd or unsupported token. Collect those DURING the pass rather than
// scanning the result: a scan of the output cannot tell a template token from
// the same sequence appearing inside a substituted value, which is exactly
// what the single pass above is careful to leave literal.
const unresolved = new Set();
const rendered = html.replace(/\{\{[A-Z_]+\}\}/g, (token) => {
const value = replacements[token];
if (value == null) {
unresolved.add(token);
return token;
}
return value;
});
// Fail loudly, matching build-cv-html.mjs and build-cv-latex.mjs. Shipping a
// letter with a literal {{TOKEN}} in it is worse than not producing one.
if (unresolved.size) {
throw new Error(`Unresolved placeholders: ${[...unresolved].join(', ')}`);
}
return rendered;
}
/** Parse a payload, run the fact gate, and render the cover-letter PDF. */
async function main() {
const { values: args } = parseArgs({
options: {
payload: { type: "string" },
out: { type: "string" },
format: { type: "string" },
report: { type: "string" },
help: { type: "boolean", short: "h" },
},
strict: false,
});
if (args.help || !args.payload) {View on GitHub (pinned to aac998c7ed)