santifer/career-ops · error · Error

wttj: the WTTJ board is global — narrow it with `wttj

Error message

wttj: the WTTJ board is global — narrow it with `wttj: { filters: "…" }` and/or `wttj: { queries: ["…"] }`

What it means

resolveConfig refuses to run a WTTJ scan when the entry defines neither any non-empty wttj.queries nor a wttj.filters expression. Welcometothejungle's board is global, so scanning it unfiltered would return arbitrary worldwide postings; the guard forces the config to narrow the board either server-side (filters) or via search terms (queries). The error text itself documents the exact fix shape.

Solutions

  1. Add wttj.filters with an expression narrowing the board (e.g. country or jobs_profile slug)
  2. Add wttj.queries with one or more non-empty search terms (e.g. "senior backend")
  3. Verify YAML indentation: queries and filters must sit inside the entry's wttj: block, and strings must not be blank
  4. Use both: filters to constrain location/contract and queries for titles

Example fix

# before
career-ops:
  enabled: true
  wttj: {}
# after
career-ops:
  enabled: true
  wttj:
    filters: "country_slug=france"
    queries: ["backend engineer"]
Defensive patterns

Strategy: validation

Validate before calling

// check the entry before enabling the wttj provider
const w = entry?.wttj ?? {};
const queries = Array.isArray(w.queries) ? w.queries.filter(q => typeof q === 'string' && q.trim()) : [];
const filters = typeof w.filters === 'string' ? w.filters.trim() : '';
if (queries.length === 0 && !filters) {
  throw new Error('wttj entry needs wttj.filters and/or non-empty wttj.queries');
}

Type guard

function isNarrowedWttjEntry(entry) {
  const w = entry?.wttj;
  if (!w || typeof w !== 'object') return false;
  const qs = Array.isArray(w.queries) ? w.queries.filter(q => typeof q === 'string' && q.trim()) : [];
  const f = typeof w.filters === 'string' ? w.filters.trim() : '';
  return qs.length > 0 || f.length > 0;
}

Try / catch

try {
  await scanWttj(entry);
} catch (e) {
  if (e.message.includes('the WTTJ board is global')) {
    console.warn('Add wttj: { filters: "…" } or wttj: { queries: ["…"] } to the portals.yml entry.');
  } else throw e;
}

Prevention

When it happens

Trigger: A portals.yml entry enables the wttj provider but its wttj block is missing, empty, contains only blank strings, or has queries: [] with no filters — e.g. wttj: {} after trimming.

Common situations: Adding a WTTJ entry to portals.yml and forgetting the narrowing block; deleting all queries during a cleanup; copy-pasting a template entry with placeholder values that trim to empty; a YAML indentation mistake nesting queries outside the wttj key so they aren't picked up.

Understand the failure class

Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/4be753549bf97178. Report an issue: GitHub.

Appendix: source

Thrown at providers/wttj.mjs:173

      max: max || min,
      currency: typeof h.salary_currency === 'string' ? h.salary_currency.trim().toUpperCase() : '',
    };
  }
  return job;
}

/** Resolve config: queries and/or an Algolia filter expression, + per-query hit cap. */
function resolveConfig(entry) {
  const cfg = entry?.wttj && typeof entry.wttj === 'object' ? entry.wttj : {};
  const queries = Array.isArray(cfg.queries)
    ? cfg.queries.filter((q) => typeof q === 'string' && q.trim()).map((q) => q.trim())
    : [];
  const filters = typeof cfg.filters === 'string' && cfg.filters.trim() ? cfg.filters.trim() : '';
  if (filters.length > FILTERS_MAX_LEN) {
    throw new Error(`wttj: \`filters\` is too long (${filters.length} > ${FILTERS_MAX_LEN} chars)`);
  }
  if (queries.length === 0 && !filters) {
    throw new Error(
      'wttj: the WTTJ board is global — narrow it with `wttj: { filters: "…" }` and/or `wttj: { queries: ["…"] }`',
    );
  }
  // A filter expression already narrows the board server-side, so the empty query
  // ("match everything that passes the filter") is the useful default. Without a
  // filter there is nothing to narrow the board, so a query list stays mandatory.
  const effectiveQueries = queries.length > 0 ? queries : [''];
  const cap = filters ? FILTERED_MAX_HITS_CAP : MAX_HITS_CAP;
  const maxHits =
    Number.isInteger(cfg.max_hits) && cfg.max_hits > 0 ? Math.min(cfg.max_hits, cap) : DEFAULT_MAX_HITS;
  return { queries: effectiveQueries, filters, maxHits };
}

/** @type {Provider} */
export default {
  id: 'wttj',

  detect(entry) {

View on GitHub (pinned to aac998c7ed)