santifer/career-ops · error · Error
Existing candidates file at
Error message
Existing candidates file at ${candidatesPath} is not a JSON array What it means
appendCandidate expects data/reply-candidates.json to be a JSON array of candidate entries. If the file parses as valid JSON but holds any other type (object, string, number, null), it throws this error instead of overwriting the unexpected shape.
Solutions
- Inspect the file and unwrap the array (e.g. replace {"candidates":[...]} with the inner [...])
- Convert the top-level value to a plain JSON array before re-running paste-reply
- Restore from backup/git if the file was clobbered by another tool
- If the wrapper format is intentional, migrate the entries into the flat-array format paste-reply expects
Example fix
// before
{"candidates": [{"from": "recruiter@acme.com"}]}
// after
[{"from": "recruiter@acme.com"}] Defensive patterns
Strategy: validation
Validate before calling
const v = JSON.parse(readFileSync(p, 'utf-8')); if (!Array.isArray(v)) throw new Error('reply-candidates.json must be a top-level array'); Type guard
const isCandidateFile = (v) => Array.isArray(v) && v.every((e) => e && typeof e === 'object');
Try / catch
try { appendCandidate(c); } catch (e) { if (e.message.includes('is not a JSON array')) { console.error('Unwrap the file to a top-level JSON array, then retry'); } else throw e; } Prevention
- Keep the file a flat JSON array; never wrap it in an object
- After external tools write the file, verify Array.isArray on parse
- Restore from git if another tool reformatted it
- Add a schema check in scripts that share this file
When it happens
Trigger: appendCandidate(candidate) called when data/reply-candidates.json contains a JSON object (e.g. {"candidates": [...]}) or any non-array JSON value.
Common situations: Another tool or script rewrote the file as a wrapper object; user hand-wrapped the array in an object; file was seeded from a different project's format; null literal written by a failed previous run.
Related errors
- invalid golden case : need string id/jd and label.
- apify: entry has invalid field_map. Each of title, url…
- each entry needs a string "input" and "output"
- Gemini returned an invalid career-ops report
- jobicy: unexpected API response — expected
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/5038e568f13ec38b.
Report an issue: GitHub.
Appendix: source
Thrown at paste-reply.mjs:125
};
}
/**
* Append a candidate to the candidates JSON file, creating the file/array if
* missing, without disturbing any existing entries. Exported for direct unit
* testing. Returns the total candidate count after the append.
*/
export function appendCandidate(candidate, candidatesPath = CANDIDATES_PATH) {
let candidates = [];
if (fs.existsSync(candidatesPath)) {
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(candidatesPath, 'utf-8'));
} catch (e) {
throw new Error(`Could not parse existing candidates file at ${candidatesPath}: ${e.message}`);
}
if (!Array.isArray(parsed)) {
throw new Error(`Existing candidates file at ${candidatesPath} is not a JSON array`);
}
candidates = parsed;
} else {
fs.mkdirSync(path.dirname(candidatesPath), { recursive: true });
}
candidates.push(candidate);
// Write-then-rename so an interrupted write (crash, signal, disk full)
// can never leave the real candidates file truncated/corrupted.
const tmpPath = `${candidatesPath}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(candidates, null, 2), 'utf-8');
renameSyncWithRetry(tmpPath, candidatesPath);
return candidates.length;
}
// Collect subject/from/body from stdin via a single readline.Interface and a
// tiny manual state machine driven off its 'line' event.
//
// Earlier drafts chained `rl.question()` calls (one interface per prompt, orView on GitHub (pinned to aac998c7ed)