santifer/career-ops · error · Error

ashby: cannot derive API URL for ${entry.name}

Error message

ashby: cannot derive API URL for ${entry.name}

What it means

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.

Source

Thrown at providers/ashby.mjs:162

  return [...new Set(parts)].join(' · ');
}

/** @type {Provider} */
export default {
  id: 'ashby',

  detect(entry) {
    try {
      const apiUrl = resolveApiUrl(entry);
      return apiUrl ? { url: apiUrl } : null;
    } catch {
      return null;
    }
  },

  async fetch(entry, ctx) {
    const apiUrl = resolveApiUrl(entry);
    if (!apiUrl) throw new Error(`ashby: cannot derive API URL for ${entry.name}`);
    assertAshbyUrl(apiUrl);
    let lastErr;
    for (let attempt = 0; attempt <= ASHBY_RETRIES; attempt++) {
      if (attempt > 0) {
        // exponential backoff + jitter — spaces out retries to dodge Ashby rate-limiting
        const backoff = 1000 * 2 ** (attempt - 1) + Math.floor(Math.random() * 500);
        await sleep(backoff, ctx);
      }
      try {
        const json = /** @type {any} */ (await ctx.fetchJson(apiUrl, { timeoutMs: ASHBY_TIMEOUT_MS, redirect: 'error' }));
        const jobs = Array.isArray(json?.jobs) ? json.jobs : [];
        return jobs.map(/** @param {any} j */ (j) => ({
          title: j.title || '',
          url: j.jobUrl || '',
          company: entry.name,
          location: formatLocation(j),
          salary: parseCompensation(j),
          postedAt: toEpochMs(j.publishedAt),

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Add an `api:` field pointing to `https://api.ashbyhq.com/posting-api?compId=<tenant>`.
  2. Confirm the provider actually applies to this entry — if it isn't an Ashby board, switch `provider:`.
  3. Use `detect()` (or the scanner's detection pass) before calling `fetch` so unconfigured entries are skipped cleanly.

Example fix

# before
- name: Acme
  provider: ashby
  careers_url: https://careers.acme.com/

# after — explicit api: pins the board
- name: Acme
  provider: ashby
  careers_url: https://careers.acme.com/
  api: https://api.ashbyhq.com/posting-api?compId=acme
Defensive patterns

Strategy: type-guard

Validate before calling

// Use detect() before fetch so unconfigured entries are skipped, not thrown
const detected = provider.detect(entry);
if (!detected) {
  console.warn(`ashby: skipping ${entry.name} — no API URL derivable (add api: or careers_url)`);
  continue;
}
const jobs = await provider.fetch(entry, ctx);

Type guard

/** @param {any} e */
function hasAshbyApiConfig(e) {
  return typeof e?.api === 'string' && e.api.length > 0;
}

Try / catch

try {
  const apiUrl = resolveApiUrl(entry);
  if (!apiUrl) throw new Error(`ashby: cannot derive API URL for ${entry.name}`);
  // ...fetch
} catch (err) {
  if (/cannot derive API URL/.test(err.message)) {
    console.warn(`skipping ${entry.name}: ${err.message}`);  // config gap, not a crash
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: `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.

Common situations: 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.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/6fe850c64346060b. Report an issue: GitHub.