santifer/career-ops · error · Error

mycareersfuture: entry

Error message

mycareersfuture: entry "${entry?.name || '(unnamed)'}" has no mycareersfuture.keywords[] and no config/profile.yml target_roles to fall back to

What it means

mycareersfuture is a keyword-driven provider: it needs search keywords either from the entry's mycareersfuture.keywords[] or, as a fallback, from target_roles in config/profile.yml. fetch() throws this error when both sources are empty, naming the entry so the user knows which board config to fix. It fails fast instead of issuing a useless query-less request.

Solutions

  1. Add keywords to the entry in portals.yml: e.g. mycareersfuture: { keywords: ['data engineer', 'backend developer'] }.
  2. Or set target_roles in config/profile.yml to your role list so the fallback resolves.
  3. If target_roles should exist, complete profile personalization — an unpersonalized template profile yields no keywords.
  4. Re-run the scan after configuring; no code change is needed.

Example fix

// before (portals.yml)
- name: MyCareersFuture
  careers_url: https://www.mycareersfuture.gov.sg
// after
- name: MyCareersFuture
  careers_url: https://www.mycareersfuture.gov.sg
  mycareersfuture:
    keywords: ["data engineer", "backend developer"]
Defensive patterns

Strategy: validation

Validate before calling

const keywords = entry?.mycareersfuture?.keywords ?? [];
const profileRoles = config?.profile?.target_roles ?? [];
if (!keywords.length && !profileRoles.length) {
  throw new Error('mycareersfuture: configure keywords[] or profile target_roles before scanning');
}

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).includes('no mycareersfuture.keywords[]')) {
    console.error('Add keywords to the entry or set target_roles in config/profile.yml');
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: Running a scan where the mycareersfuture entry defines no keywords[] AND config/profile.yml has no target_roles (or resolveProfileKeywords() returns empty because the profile file is missing/unpersonalized).

Common situations: A fresh onboarding where config/profile.yml still contains placeholder/empty target_roles; a hand-written portals.yml entry for mycareersfuture that omitted keywords; a renamed profile field after a config schema change so resolveProfileKeywords() finds nothing.

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/3d712ff8301e453d. Report an issue: GitHub.

Appendix: source

Thrown at providers/mycareersfuture.mjs:167

  },

  /**
   * Fetches and normalizes postings from MyCareersFuture's public search API.
   * @param {{ name?: string, mycareersfuture?: any, max_pages?: unknown }} entry
   * @param {{ fetchJson: (url: string, opts?: object) => Promise<any>, maxPages?: number }} ctx
   * @returns {Promise<Array<{title: string, url: string, company: string, location: string, postedAt?: number}>>}
   */
  async fetch(entry, ctx) {
    const { size, maxPages: configuredMaxPages, keywords: ownKeywords } = parseConfig(entry);
    let keywords = ownKeywords;
    // Fall back to config/profile.yml's target_roles when this entry has no
    // mycareersfuture.keywords[] of its own — same convention vdab.mjs and
    // jobbankca.mjs use, so a user who already onboarded with target roles
    // doesn't have to duplicate them into every keyword-required provider's
    // config by hand.
    if (!keywords.length) keywords = resolveProfileKeywords();
    if (!keywords.length) {
      throw new Error(`mycareersfuture: entry "${entry?.name || '(unnamed)'}" has no mycareersfuture.keywords[] and no config/profile.yml target_roles to fall back to`);
    }

    // Same probe-vs-real-scan split as vdab.mjs/jobbankca.mjs: verify-portals.mjs's
    // bounded health probe passes ctx.maxPages so a liveness check can't walk
    // the whole board; a real scan (ctx.maxPages unset) uses the configured cap.
    const probing = Number.isInteger(ctx?.maxPages) && ctx.maxPages > 0;
    const pageLimit = probing ? Math.min(ctx.maxPages, configuredMaxPages) : configuredMaxPages;

    /** @param {string} keyword */
    const fetchKeyword = async (keyword) => {
      const out = [];
      for (let page = 0; page < pageLimit; page++) {
        const json = await fetchJsonWithRetry(ctx, `${API_URL}?limit=${size}&page=${page}`, {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ search: keyword, sortBy: ['new_posting_date'], page }),
          redirect: 'error',
          timeoutMs: 12_000,

View on GitHub (pinned to aac998c7ed)