santifer/career-ops · error · Error
workable: cannot derive feed URL for
Error message
workable: cannot derive feed URL for ${entry.name} What it means
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.
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
Example fix
// before
{ name: 'Acme', careers_url: 'https://acme.com/jobs' }
// after
{ name: 'Acme', careers_url: 'https://apply.workable.com/acme/' } Defensive patterns
Strategy: validation
Validate before calling
function hasWorkableSlug(entry) {
const src = entry.api || entry.careers_url || '';
return /apply\.workable\.com\/([a-z0-9_-]+)/i.test(src);
}
if (!hasWorkableSlug(entry)) console.warn(`Skip ${entry.name}: no Workable slug derivable`); Type guard
const hasWorkableSlug = (e) => /apply\.workable\.com\/([a-z0-9_-]+)/i.test(e.api || e.careers_url || '');
Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/90e7561b122ebf44.
Report an issue: GitHub.
Appendix: source
Thrown at providers/workable.mjs:157
if (!slug || !SLUG_RE.test(slug)) return null;
return slug;
}
const widgetUrlFor = (slug) => `https://apply.workable.com/api/v1/widget/accounts/${slug}?details=true`;
const feedUrlFor = (slug) => `https://apply.workable.com/${slug}/jobs.md`;
/** @type {Provider} */
export default {
id: 'workable',
detect(entry) {
const slug = resolveWorkableSlug(entry);
return slug ? { url: widgetUrlFor(slug) } : null;
},
async fetch(entry, ctx) {
const slug = resolveWorkableSlug(entry);
if (!slug) throw new Error(`workable: cannot derive feed URL for ${entry.name}`);
const referer = `https://apply.workable.com/${slug}/`;
return serialized(async () => {
// Primary: widget API. assertWorkableUrl + redirect:'error' together
// guarantee the final hostname stays in the allowlist (no SSRF via
// redirect). Retries transient failures; gives up early on a
// long-lived Retry-After so a Cloudflare-level block on this path
// falls through to the markdown feed instead of stalling the scan.
const apiUrl = assertWorkableUrl(widgetUrlFor(slug));
let payload = null;
try {
payload = await fetchWithRetry(ctx, () => ctx.fetchJson(apiUrl, {
redirect: 'error',
headers: { ...WORKABLE_HEADERS, referer },
}));
} catch {
payload = null; // fall through to the markdown feedView on GitHub (pinned to aac998c7ed)