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

  1. Add keywords to the portal entry, e.g. vdab: { keywords: ['software engineer', 'data engineer'] }
  2. Or set target_roles in config/profile.yml (e.g. target_roles: ['Senior Backend Engineer']) so the fallback resolves
  3. Check config/profile.yml is actually being loaded — a wrong working directory or typo'd path means resolveProfileKeywords() returns nothing
  4. 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

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


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)