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
- Add keywords to the entry in portals.yml: e.g. mycareersfuture: { keywords: ['data engineer', 'backend developer'] }.
- Or set target_roles in config/profile.yml to your role list so the fallback resolves.
- If target_roles should exist, complete profile personalization — an unpersonalized template profile yields no keywords.
- 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
- Personalize config/profile.yml target_roles during onboarding — the fallback depends on it.
- Give keyword-required providers explicit keywords[] in portals.yml rather than relying on the profile fallback.
- Run a config lint before scans to catch entries missing required keyword fields.
- Re-check provider config after profile schema changes; renamed fields silently empty the fallback.
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
- jobbankca: entry " " has no jobbankca.keywords[] and no…
- apify: entry has invalid field_map. Each of title, url…
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
- APIFY_TOKEN not set — enable apify in config/plugins.yml…
- arbeitnow: invalid URL
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)