{"record":{"id":"6fe850c64346060b","repo":"santifer/career-ops","slug":"ashby-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"ashby: cannot derive API URL for ${entry.name}","messagePattern":"ashby: cannot derive API URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/ashby.mjs","lineNumber":162,"sourceCode":"  return [...new Set(parts)].join(' · ');\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'ashby',\n\n  detect(entry) {\n    try {\n      const apiUrl = resolveApiUrl(entry);\n      return apiUrl ? { url: apiUrl } : null;\n    } catch {\n      return null;\n    }\n  },\n\n  async fetch(entry, ctx) {\n    const apiUrl = resolveApiUrl(entry);\n    if (!apiUrl) throw new Error(`ashby: cannot derive API URL for ${entry.name}`);\n    assertAshbyUrl(apiUrl);\n    let lastErr;\n    for (let attempt = 0; attempt <= ASHBY_RETRIES; attempt++) {\n      if (attempt > 0) {\n        // exponential backoff + jitter — spaces out retries to dodge Ashby rate-limiting\n        const backoff = 1000 * 2 ** (attempt - 1) + Math.floor(Math.random() * 500);\n        await sleep(backoff, ctx);\n      }\n      try {\n        const json = /** @type {any} */ (await ctx.fetchJson(apiUrl, { timeoutMs: ASHBY_TIMEOUT_MS, redirect: 'error' }));\n        const jobs = Array.isArray(json?.jobs) ? json.jobs : [];\n        return jobs.map(/** @param {any} j */ (j) => ({\n          title: j.title || '',\n          url: j.jobUrl || '',\n          company: entry.name,\n          location: formatLocation(j),\n          salary: parseCompensation(j),\n          postedAt: toEpochMs(j.publishedAt),","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/ashby.mjs#L144-L180","documentation":"ashby's `fetch` calls `resolveApiUrl(entry)` to find the posting-API URL. If it returns a falsy value (no `entry.api` and no derivable board URL), `fetch` throws naming the entry. This is a hard config error — there is no endpoint to hit.","triggerScenarios":"`resolveApiUrl(entry)` returns null/undefined: the entry has no `api:` field and no other field from which the Ashby board URL can be derived. Thrown at the top of `fetch` before any network call. Note `detect()` catches the same failure and returns null (skipping the provider) — this throw only fires if `fetch` is invoked directly.","commonSituations":"An entry lists ashby as its provider but omits the `api:` field; a YAML indentation error separated `api` from its entry; or the entry was created from a template that left the API URL blank.","solutions":["Add an `api:` field pointing to `https://api.ashbyhq.com/posting-api?compId=<tenant>`.","Confirm the provider actually applies to this entry — if it isn't an Ashby board, switch `provider:`.","Use `detect()` (or the scanner's detection pass) before calling `fetch` so unconfigured entries are skipped cleanly."],"exampleFix":"# before\n- name: Acme\n  provider: ashby\n  careers_url: https://careers.acme.com/\n\n# after — explicit api: pins the board\n- name: Acme\n  provider: ashby\n  careers_url: https://careers.acme.com/\n  api: https://api.ashbyhq.com/posting-api?compId=acme","handlingStrategy":"type-guard","validationCode":"// Use detect() before fetch so unconfigured entries are skipped, not thrown\nconst detected = provider.detect(entry);\nif (!detected) {\n  console.warn(`ashby: skipping ${entry.name} — no API URL derivable (add api: or careers_url)`);\n  continue;\n}\nconst jobs = await provider.fetch(entry, ctx);","typeGuard":"/** @param {any} e */\nfunction hasAshbyApiConfig(e) {\n  return typeof e?.api === 'string' && e.api.length > 0;\n}","tryCatchPattern":"try {\n  const apiUrl = resolveApiUrl(entry);\n  if (!apiUrl) throw new Error(`ashby: cannot derive API URL for ${entry.name}`);\n  // ...fetch\n} catch (err) {\n  if (/cannot derive API URL/.test(err.message)) {\n    console.warn(`skipping ${entry.name}: ${err.message}`);  // config gap, not a crash\n    return [];\n  }\n  throw err;\n}","preventionTips":["Always call `detect()` before `fetch()` so missing config is skipped gracefully.","Require an explicit `api:` field in ashby entries during config validation.","Surface a config report listing entries where detect() returned null."],"tags":["ashby","config","missing-field","validation"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}