{"record":{"id":"9dfb2dda44f8643a","repo":"santifer/career-ops","slug":"vdab-entry-entry-name-unnamed-has-no","errorCode":null,"errorMessage":"vdab: entry \"${entry.name || '(unnamed)'}\" has no vdab.keywords[] and no config/profile.yml target_roles to fall back to","messagePattern":"vdab: entry \"(.+?)\" has no vdab\\.keywords\\[\\] and no config/profile\\.yml target_roles to fall back to","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/vdab.mjs","lineNumber":194,"sourceCode":"export default {\n  id: 'vdab',\n\n  /**\n   * Fetches and normalizes postings from VDAB's vacatureLight search API.\n   * @param {{ name?: string, vdab?: any }} entry\n   * @param {{ fetchJson: (url: string, opts?: object) => Promise<any>, fetchText: (url: string, opts?: object) => Promise<string> }} ctx\n   * @returns {Promise<Array<{title: string, url: string, company: string, location: string, postedAt?: number}>>}\n   */\n  async fetch(entry, ctx) {\n    const { days, size, fetchDetails, detailLimit, keywords: ownKeywords } = parseVdabConfig(entry);\n    let keywords = ownKeywords;\n    // Fall back to config/profile.yml's target_roles when this entry has no\n    // vdab.keywords[] of its own — most users who onboarded already have\n    // target roles recorded, so this avoids duplicating that into every\n    // keyword-required provider's config by hand.\n    if (!keywords.length) keywords = resolveProfileKeywords();\n    if (!keywords.length) {\n      throw new Error(`vdab: entry \"${entry.name || '(unnamed)'}\" has no vdab.keywords[] and no config/profile.yml target_roles to fall back to`);\n    }\n\n    // Scoped to this fetch() call: try the hardcoded key first (fast path);\n    // on a 403 (VDAB rotated it), re-derive once from the live bundle and\n    // keep using the fresh key for every remaining request this run.\n    let activeKey = VEJ_KEY_MONITOR;\n    let rederiveAttempted = false;\n\n    /**\n     * Runs a VDAB JSON request with the active public frontend key. If VDAB\n     * rotates that key, re-derive it once from the live bundle and retry.\n     *\n     * @param {string} url\n     * @param {object} requestOpts\n     */\n    const keyedFetchJson = async (url, requestOpts) => {\n      const opts = {\n        ...requestOpts,","sourceCodeStart":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/vdab.mjs#L176-L212","documentation":"The VDAB provider queries Flanders' public employment service search API, which is keyword-driven (there is no board-wide listing endpoint). It resolves search terms from entry.vdab.keywords[], then falls back to config/profile.yml target_roles.primary[] plus target_roles.archetypes[].name via _profile-keywords.mjs. This throws when both sources are empty after trimming/dedup, because a keywordless POST would return an unbounded nationwide dump.","triggerScenarios":"A job_boards/tracked_companies entry with provider: vdab whose vdab: block is absent or whose vdab.keywords is missing/empty/all-whitespace, AND config/profile.yml does not exist, fails to parse (resolveProfileKeywords fails open to []), or has an empty target_roles block. The \"(unnamed)\" variant fires when entry.name is also unset.","commonSituations":"User copied a VDAB sample entry but forgot to fill in keywords; onboarding started but target_roles never populated; profile.yml has a YAML indentation error that silently fails to parse; the entry was renamed so its name reads (unnamed).","solutions":["Add a vdab: { keywords: [\"Machine Learning Engineer\", \"Data Scientist\"] } block to the portals.yml entry.","Alternatively populate target_roles: { primary: [...] } in config/profile.yml so every keyword-required provider inherits it.","Verify config/profile.yml parses cleanly, since resolveProfileKeywords swallows parse errors and returns [] (run node doctor.mjs --json).","Confirm the entry has enabled: true and provider: vdab exactly."],"exampleFix":"# before\n- name: VDAB — AI/ML Vlaanderen\n  provider: vdab\n  enabled: true\n# after\n- name: VDAB — AI/ML Vlaanderen\n  provider: vdab\n  enabled: true\n  vdab:\n    keywords: [\"Machine Learning Engineer\", \"Data Scientist\"]\n    days: 30\n    size: 100","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"fs\";\nimport * as yaml from \"js-yaml\";\n\nfunction vdabEntryHasKeywords(entry, profilePath = \"config/profile.yml\") {\n  const kws = Array.isArray(entry?.vdab?.keywords)\n    ? entry.vdab.keywords.filter(k => typeof k === \"string\" && k.trim()).map(k => k.trim())\n    : [];\n  if (kws.length) return true;\n  try {\n    const p = yaml.load(readFileSync(profilePath, \"utf8\")) || {};\n    const roles = p?.target_roles || {};\n    const fallback = [\n      ...(Array.isArray(roles.primary) ? roles.primary : []),\n      ...(Array.isArray(roles.archetypes) ? roles.archetypes.map(a => a?.name) : []),\n    ].filter(s => typeof s === \"string\" && s.trim());\n    return fallback.length > 0;\n  } catch { return false; }\n}\n\n// before invoking the provider:\nif (entry.provider === \"vdab\" && !vdabEntryHasKeywords(entry)) {\n  console.warn(`skip ${entry.name}: add vdab.keywords or target_roles`);\n  continue;\n}","typeGuard":"const isNonEmptyStringArray = (v) =>\n  Array.isArray(v) && v.every(x => typeof x === \"string\") && v.some(x => x.trim());","tryCatchPattern":null,"preventionTips":["Always pair a provider: vdab entry with a vdab.keywords block, even if target_roles is set — keeps the entry self-describing.","Run node doctor.mjs --json after onboarding to confirm config/profile.yml is present and parseable.","Lint portals.yml in CI for any provider: vdab row lacking keywords."],"tags":["config","vdab","validation","onboarding","portals-yml"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}