{"record":{"id":"90e7561b122ebf44","repo":"santifer/career-ops","slug":"workable-cannot-derive-feed-url-for-entry-name","errorCode":null,"errorMessage":"workable: cannot derive feed URL for ${entry.name}","messagePattern":"workable: cannot derive feed URL for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/workable.mjs","lineNumber":157,"sourceCode":"  if (!slug || !SLUG_RE.test(slug)) return null;\n  return slug;\n}\n\nconst widgetUrlFor = (slug) => `https://apply.workable.com/api/v1/widget/accounts/${slug}?details=true`;\nconst feedUrlFor = (slug) => `https://apply.workable.com/${slug}/jobs.md`;\n\n/** @type {Provider} */\nexport default {\n  id: 'workable',\n\n  detect(entry) {\n    const slug = resolveWorkableSlug(entry);\n    return slug ? { url: widgetUrlFor(slug) } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const slug = resolveWorkableSlug(entry);\n    if (!slug) throw new Error(`workable: cannot derive feed URL for ${entry.name}`);\n\n    const referer = `https://apply.workable.com/${slug}/`;\n\n    return serialized(async () => {\n      // Primary: widget API. assertWorkableUrl + redirect:'error' together\n      // guarantee the final hostname stays in the allowlist (no SSRF via\n      // redirect). Retries transient failures; gives up early on a\n      // long-lived Retry-After so a Cloudflare-level block on this path\n      // falls through to the markdown feed instead of stalling the scan.\n      const apiUrl = assertWorkableUrl(widgetUrlFor(slug));\n      let payload = null;\n      try {\n        payload = await fetchWithRetry(ctx, () => ctx.fetchJson(apiUrl, {\n          redirect: 'error',\n          headers: { ...WORKABLE_HEADERS, referer },\n        }));\n      } catch {\n        payload = null; // fall through to the markdown feed","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/workable.mjs#L139-L175","documentation":"The Workable provider's fetch needs an account slug to build the widget feed URL. resolveWorkableSlug derives it from the entry (api or careers_url); when it returns null, fetch throws because it cannot construct a feed URL for that tracked company.","triggerScenarios":"fetch(entry, ctx) called with an entry that has neither a parseable api URL nor a careers_url containing a Workable slug — e.g. `{ name: 'Acme' }` with no api/careers_url, or a careers_url from a different ATS.","commonSituations":"A portals.yml entry for a company whose careers page is not Workable; an entry missing the api field; a careers_url format change after the company migrated ATS.","solutions":["Add a valid Workable careers_url (https://apply.workable.com/<slug>/) or api field to the entry","Verify the company actually uses Workable; if not, route it to the correct provider","Remove or disable the entry if the company no longer has a Workable board"],"exampleFix":"// before\n{ name: 'Acme', careers_url: 'https://acme.com/jobs' }\n// after\n{ name: 'Acme', careers_url: 'https://apply.workable.com/acme/' }","handlingStrategy":"validation","validationCode":"function hasWorkableSlug(entry) {\n  const src = entry.api || entry.careers_url || '';\n  return /apply\\.workable\\.com\\/([a-z0-9_-]+)/i.test(src);\n}\nif (!hasWorkableSlug(entry)) console.warn(`Skip ${entry.name}: no Workable slug derivable`);","typeGuard":"const hasWorkableSlug = (e) => /apply\\.workable\\.com\\/([a-z0-9_-]+)/i.test(e.api || e.careers_url || '');","tryCatchPattern":"try { jobs = await workable.fetch(entry, ctx); } catch (e) { if (e.message.startsWith('workable: cannot derive feed URL')) { console.warn(`${entry.name}: add a Workable careers_url or route to another provider`); jobs = []; } else throw e; }","preventionTips":["Keep the api/careers_url fields populated for every Workable entry","Verify each company's ATS before adding it to portals.yml","Drop entries whose careers page is not Workable rather than leaving them half-configured"],"tags":["provider-config","missing-slug","feed-url","workable"],"backgroundTag":"missing-required-config-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}