{"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":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/vdab.mjs","lineNumber":200,"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":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/vdab.mjs#L182-L218","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (portals.yml)\n- provider: vdab\n  name: vdab-flanders\n// after\n- provider: vdab\n  name: vdab-flanders\n  vdab:\n    keywords: [\"software engineer\", \"backend developer\"]","handlingStrategy":"validation","validationCode":"function hasVdabKeywords(entry, profile) {\n  const kws = entry?.vdab?.keywords;\n  if (Array.isArray(kws) && kws.length > 0) return true;\n  const roles = profile?.target_roles;\n  return Array.isArray(roles) && roles.length > 0;\n}\n// check before calling fetch: hasVdabKeywords(entry, profile)","typeGuard":"const hasKeywords = (e) => Array.isArray(e?.vdab?.keywords) && e.vdab.keywords.length > 0;","tryCatchPattern":"try {\n  await vdabProvider.fetch(entry, ctx);\n} catch (err) {\n  if (err.message.includes('no vdab.keywords[] and no config/profile.yml target_roles')) {\n    console.error(`Add keywords for ${entry.name} in portals.yml, or set target_roles in config/profile.yml`);\n    return;\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["config","missing-keywords","provider","onboarding"],"backgroundTag":"missing-required-config-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}