santifer/career-ops · error · Error
vdab: entry " " has no vdab.keywords[] and no…
Error message
vdab: entry "${entry.name || '(unnamed)'}" has no vdab.keywords[] and no config/profile.yml target_roles to fall back to What it means
The vdab provider requires a non-empty keyword list to build its searches. It first reads the entry's own vdab.keywords[], and if that is empty it falls back to target_roles from config/profile.yml. If BOTH are empty/missing, fetch() throws this error rather than issuing a meaningless query to the VDAB API.
Solutions
- Add keywords to the portal entry, e.g. vdab: { keywords: ['software engineer', 'data engineer'] }
- Or set target_roles in config/profile.yml (e.g. target_roles: ['Senior Backend Engineer']) so the fallback resolves
- Check config/profile.yml is actually being loaded — a wrong working directory or typo'd path means resolveProfileKeywords() returns nothing
- If the entry name in the error is '(unnamed)', give the entry a name to make future config errors diagnosable
Example fix
// before (portals.yml)
- provider: vdab
name: vdab-flanders
// after
- provider: vdab
name: vdab-flanders
vdab:
keywords: ["software engineer", "backend developer"] Defensive patterns
Strategy: validation
Validate before calling
function hasVdabKeywords(entry, profile) {
const kws = entry?.vdab?.keywords;
if (Array.isArray(kws) && kws.length > 0) return true;
const roles = profile?.target_roles;
return Array.isArray(roles) && roles.length > 0;
}
// check before calling fetch: hasVdabKeywords(entry, profile) Type guard
const hasKeywords = (e) => Array.isArray(e?.vdab?.keywords) && e.vdab.keywords.length > 0;
Try / catch
try {
await vdabProvider.fetch(entry, ctx);
} catch (err) {
if (err.message.includes('no vdab.keywords[] and no config/profile.yml target_roles')) {
console.error(`Add keywords for ${entry.name} in portals.yml, or set target_roles in config/profile.yml`);
return;
}
throw err;
} Prevention
- After onboarding, confirm config/profile.yml has a non-empty target_roles list
- When adding a vdab entry to portals.yml, always include vdab.keywords explicitly — do not rely on the profile fallback
- Give every portal entry a name so config errors identify the culprit instead of '(unnamed)'
- Run a config dry-run that instantiates all providers before scheduled scans
When it happens
Trigger: Calling the vdab provider's fetch() with a job_boards entry that defines no vdab.keywords[] array (or an empty one), while config/profile.yml also has no target_roles key — or target_roles is present but an empty list.
Common situations: A fresh onboarding where config/profile.yml was copied from the example but target_roles was never filled in; a vdab portal entry added by hand with only provider/name set; target_roles renamed or moved in a profile.yml refactor leaving the provider's fallback dangling.
Understand the failure class
Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.
Related errors
- arbeitnow: invalid URL
- arbeitsagentur: entry
- ashby: cannot derive API URL for
- avature: cannot resolve origin for
- bamboohr: cannot derive API URL for
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/9dfb2dda44f8643a.
Report an issue: GitHub.
Appendix: source
Thrown at providers/vdab.mjs:200
export default {
id: 'vdab',
/**
* Fetches and normalizes postings from VDAB's vacatureLight search API.
* @param {{ name?: string, vdab?: any }} entry
* @param {{ fetchJson: (url: string, opts?: object) => Promise<any>, fetchText: (url: string, opts?: object) => Promise<string> }} ctx
* @returns {Promise<Array<{title: string, url: string, company: string, location: string, postedAt?: number}>>}
*/
async fetch(entry, ctx) {
const { days, size, fetchDetails, detailLimit, keywords: ownKeywords } = parseVdabConfig(entry);
let keywords = ownKeywords;
// Fall back to config/profile.yml's target_roles when this entry has no
// vdab.keywords[] of its own — most users who onboarded already have
// target roles recorded, so this avoids duplicating that into every
// keyword-required provider's config by hand.
if (!keywords.length) keywords = resolveProfileKeywords();
if (!keywords.length) {
throw new Error(`vdab: entry "${entry.name || '(unnamed)'}" has no vdab.keywords[] and no config/profile.yml target_roles to fall back to`);
}
// Scoped to this fetch() call: try the hardcoded key first (fast path);
// on a 403 (VDAB rotated it), re-derive once from the live bundle and
// keep using the fresh key for every remaining request this run.
let activeKey = VEJ_KEY_MONITOR;
let rederiveAttempted = false;
/**
* Runs a VDAB JSON request with the active public frontend key. If VDAB
* rotates that key, re-derive it once from the live bundle and retry.
*
* @param {string} url
* @param {object} requestOpts
*/
const keyedFetchJson = async (url, requestOpts) => {
const opts = {
...requestOpts,View on GitHub (pinned to aac998c7ed)