{"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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/workable.mjs#L139-L175","documentation":"Workable auto-detects an account from a careers_url matching https://apply.workable.com/<slug>; resolveWorkableSlug returns null when the URL is absent, not https, not on apply.workable.com, or the slug fails SLUG_RE. fetch() then throws because it has no slug to build the widget/feed URL from.","triggerScenarios":"A tracked_companies/job_boards entry with provider: workable whose careers_url is missing, is a branded careers domain (e.g. https://www.acme.com/careers), uses http, or whose slug contains characters outside [A-Za-z0-9_-] (leading dot/slash, spaces).","commonSituations":"User sets provider: workable on a company whose careers page is a custom domain; the entry relies on detection but has no careers_url; the slug has leading punctuation.","solutions":["Set careers_url: https://apply.workable.com/<slug> on the entry (find the slug in the company's Workable URL).","If the company uses a branded domain, do not set provider: workable — let a different detection path handle it or drop the entry.","Confirm the slug is alphanumeric / dash / underscore with no leading punctuation."],"exampleFix":"# before\n- name: Acme\n  provider: workable\n# after\n- name: Acme\n  provider: workable\n  careers_url: https://apply.workable.com/acme","handlingStrategy":"validation","validationCode":"import { resolveWorkableSlug } from \"./providers/workable.mjs\";\n// before scan\nif (entry.provider === \"workable\") {\n  const slug = resolveWorkableSlug(entry);\n  if (!slug) { console.warn(`skip ${entry.name}: no workable slug in careers_url`); continue; }\n}","typeGuard":"const hasWorkableSlug = (e) => typeof resolveWorkableSlug(e) === \"string\";","tryCatchPattern":null,"preventionTips":["Always pair provider: workable with a careers_url on apply.workable.com.","Lint portals.yml entries for a resolvable slug before scanning."],"tags":["config","workable","detection","portals-yml","url-pattern"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}