santifer/career-ops · error · Error
Unresolved placeholders: ${[...unresolved].join(', ')}
Error message
Unresolved placeholders: ${[...unresolved].join(', ')} What it means
buildHtml does a single-pass regex substitution of {{TOKEN}} patterns against a replacements map. After the pass, any remaining {{TOKEN}} in the output means the template used a token the renderer does not support. This fails loudly because shipping a cover letter with a literal {{TOKEN}} visible to the recipient is worse than not producing one. The supported tokens are listed in the replacements map: 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.
Source
Thrown at generate-cover-letter.mjs:222
// 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 9b17a8ac97)
Solutions
- Check the custom cover-letter template for {{TOKEN}} patterns and ensure each one is in the supported set (NAME, CONTACT_LINE, ROLE_TITLE, OPENING, PROFILE_INTRO, etc.).
- Fix typos: {{NANE}} → {{NAME}}, {{ROL_TITLE}} → {{ROLE_TITLE}}.
- Remove unsupported tokens from the template, or extend the replacements map in buildHtml if the token is intentionally new.
Example fix
<!-- before (custom template has a typo) -->
<h1>{{NANE}}</h1>
<p>{{ROLE_TITEL}}</p>
<!-- after -->
<h1>{{NAME}}</h1>
<p>{{ROLE_TITLE}}</p> Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync } from 'fs';
const SUPPORTED_TOKENS = new Set([
'{{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}}',
]);
function validateTemplateTokens(templatePath) {
const html = readFileSync(templatePath, 'utf-8');
const tokens = [...html.matchAll(/\{\{[A-Z_]+\}\}/g)].map(m => m[0]);
const unsupported = tokens.filter(t => !SUPPORTED_TOKENS.has(t));
if (unsupported.length) {
throw new Error(`Unsupported tokens in template: ${[...new Set(unsupported)].join(', ')}`);
}
} Try / catch
try {
const html = buildHtml(payload, templatePath);
} catch (err) {
if (err.message.startsWith('Unresolved placeholders')) {
console.error('Template has unsupported tokens:', err.message);
// Fix the template, then retry
process.exit(1);
}
throw err;
} Prevention
- After installing or editing a custom cover-letter template, validate its tokens against the supported set.
- Watch for typos: {{NANE}}, {{ROL_TITLE}}, etc.
- When upgrading career-ops, re-validate custom templates — token names may have changed.
When it happens
Trigger: A custom cover-letter template (installed via cv-templates.mjs KINDS.cover) contains {{SOMETHING_NEW}} that the renderer does not know about; a typo in the template like {{NANE}} instead of {{NAME}}; the template references a token that was renamed or removed in a newer version.
Common situations: User installs a custom cover-letter template pack with extra tokens; an agent edits the template and introduces a typo; a version update renamed a token but the custom template was not migrated.
Related errors
- Missing required field: ${context}.${key}
- CV section order diverges from cv.md: rendered ${renderedOrd
- payload must include at least one of: cv, articleDigest
- payload.cv requires { section, entry }
- payload.cv requires a non-empty dedupKey (used for dedup/ide
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/560b72bfef253e99.
Report an issue: GitHub.