santifer/career-ops · error
arbeitsagentur: entry
Error message
arbeitsagentur: entry "${entry.name || '(unnamed)'}" has no arbeitsagentur.keywords[] What it means
The arbeitsagentur provider requires each portals.yml entry using it to define arbeitsagentur.keywords[] — the list of search terms driving the 'was' query parameter. An entry with no keywords cannot produce any requests, so fetch throws immediately naming the entry.
Solutions
- Add arbeitsagentur.keywords to the entry named in the message, e.g. arbeitsagentur: { keywords: ['Softwareentwickler'], umkreis: 50 }.
- Check YAML indentation — keywords must sit under the arbeitsagentur key of that entry, not a sibling level.
- Verify the key spelling is exactly `keywords` (plural, array) and that entries are non-empty strings.
- Run node audit-portals.mjs or verify-pipeline.mjs to catch misconfigured entries before a scan run.
Example fix
# before
- name: bundesinnenministerium
provider: arbeitsagentur
location: Berlin
# after
- name: bundesinnenministerium
provider: arbeitsagentur
arbeitsagentur:
keywords: ["IT-Projektmanagement", "Softwareentwickler"]
wo: Berlin
umkreis: 50 Defensive patterns
Strategy: validation
Validate before calling
for (const entry of config.job_boards) {
if (entry.provider === 'arbeitsagentur') {
const kws = entry.arbeitsagentur?.keywords;
if (!Array.isArray(kws) || kws.length === 0 || !kws.every(k => typeof k === 'string' && k.trim())) {
throw new Error(`entry "${entry.name}" needs a non-empty arbeitsagentur.keywords[] array`);
}
}
} Type guard
function hasArbeitsagenturKeywords(entry) {
const kws = entry?.arbeitsagentur?.keywords;
return Array.isArray(kws) && kws.length > 0 && kws.every(k => typeof k === 'string' && k.trim().length > 0);
} Try / catch
try {
await provider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).includes('has no arbeitsagentur.keywords')) {
console.error(`Fix portals.yml: entry "${entry.name}" is missing arbeitsagentur.keywords[] (check indentation and plural key)`);
}
throw err;
} Prevention
- Run node verify-pipeline.mjs / audit-portals.mjs after editing portals.yml — they catch missing provider-specific fields before a scan.
- Copy arbeitsagentur entries from a working example so the keywords block and indentation carry over.
- Remember other providers don't need keywords; each provider has its own required block — check the provider header comment.
When it happens
Trigger: A job_boards entry with provider: arbeitsagentur lacks the arbeitsagentur block, has keywords: [] (or an empty/whitespace-only list after parseArbeitsagenturConfig filters), or misspells the key (keyword: instead of keywords:) so it parses to an empty array.
Common situations: Copy-pasting an entry from another provider (arbeitnow/agentic-jobs don't need keywords) without adding the arbeitsagentur block; a YAML indentation mistake nesting keywords under the wrong parent; renaming the key during cleanup; adding a new board entry and forgetting the required field.
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
- breezy: cannot derive API URL for
- gem: cannot derive board id for
- jibeapply: careers_url required
- rippling: cannot derive API URL for
- solidjobs: careers_url required
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/69f1113260db9ae6.
Report an issue: GitHub.
Appendix: source
Thrown at providers/arbeitsagentur.mjs:160
// candidate whose title makes no remote claim keeps its real city, which is the
// fail-closed behaviour an unverifiable lookup had in v4: the `Deutschlandweit
// (Homeoffice)` marker exempts a job from the commute location_filter, so
// tagging on nv_true alone would smuggle every hybrid past it.
/** @type {Provider} */
export default {
id: 'arbeitsagentur',
/**
* Fetches and normalizes postings from the Arbeitsagentur Jobsuche API.
* @param {{ name?: string, arbeitsagentur?: any }} entry
* @param {{ fetchJson: (url: string, opts?: object) => Promise<any> }} ctx
* @returns {Promise<Array<{title: string, url: string, company: string, location: string}>>}
*/
async fetch(entry, ctx) {
const { keywords, wo, umkreis, days, size, remoteNationwide, remoteMatch, remoteMaxPages } = parseArbeitsagenturConfig(entry);
if (!keywords.length) {
throw new Error(`arbeitsagentur: entry "${entry.name || '(unnamed)'}" has no arbeitsagentur.keywords[]`);
}
/** @param {string} was @param {Record<string,string>} [extra] */
const fetchKeyword = async (was, extra = {}) => {
const params = new URLSearchParams({
was,
size: String(size),
page: '1',
angebotsart: '1', // 1 = ARBEIT (employment; excludes Ausbildung/Selbständigkeit)
veroeffentlichtseit: String(days),
...extra,
});
// redirect:'error' prevents SSRF via server-side redirects.
const json = await ctx.fetchJson(`${API_URL}?${params.toString()}`, {
headers: { 'X-API-Key': API_KEY, accept: 'application/json' },
redirect: 'error',
timeoutMs: 12_000,
});View on GitHub (pinned to aac998c7ed)