santifer/career-ops · error · Error

gem: cannot derive board id for ${entry.name}

Error message

gem: cannot derive board id for ${entry.name}

What it means

gem.mjs throws this at the top of fetch() when resolveBoardId(entry) returns null. resolveBoardId extracts the board id from entry.careers_url: it must be a parseable URL whose hostname is exactly jobs.gem.com and whose first path segment is the board id. If careers_url is missing, not a jobs.gem.com URL, or has no path segment, no board id can be derived and the provider refuses to fetch.

Source

Thrown at providers/gem.mjs:153

function formatLocation(loc) {
  const parts = [];
  if (typeof loc?.name === 'string' && loc.name.trim()) parts.push(loc.name.trim());
  if (loc?.isRemote) parts.push('Remote');
  return parts.join(' · ');
}

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

  detect(entry) {
    const boardId = resolveBoardId(entry);
    return boardId ? { url: `${GEM_API_URL}?board=${boardId}` } : null;
  },

  async fetch(entry, ctx) {
    const boardId = resolveBoardId(entry);
    if (!boardId) throw new Error(`gem: cannot derive board id for ${entry.name}`);
    assertGemUrl(GEM_API_URL);

    const body = JSON.stringify([
      { operationName: 'JobBoardList', variables: { boardId }, query: JOB_BOARD_LIST_QUERY },
    ]);
    // redirect:'error' prevents SSRF via server-side redirects; combined with
    // assertGemUrl above it guarantees the final hostname stays in the allowlist.
    const json = /** @type {any} */ (await ctx.fetchJson(GEM_API_URL, {
      method: 'POST',
      headers: { 'content-type': 'application/json', batch: 'true' },
      body,
      redirect: 'error',
    }));

    const listResult = json?.[0];
    if (Array.isArray(listResult?.errors) && listResult.errors.length > 0) {
      throw new Error(`gem: JobBoardList failed: ${listResult.errors[0]?.message || 'unknown GraphQL error'}`);
    }

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Open the company's Gem board in a browser and copy the URL whose host is jobs.gem.com and whose first path segment is the board id (e.g. https://jobs.gem.com/acme).
  2. Set careers_url to that URL in portals.yml for the provider: gem entry.
  3. If you only know a non-Gem careers page, do not use provider: gem — pick the provider that actually backs that URL.
  4. Verify there are no leading/trailing spaces or quotes around careers_url in the YAML.

Example fix

# before
- name: Acme
  provider: gem
  careers_url: https://acme.com/careers   # not a Gem board -> throws

# after
- name: Acme
  provider: gem
  careers_url: https://jobs.gem.com/acme
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight check for a portals.yml entry before invoking the gem provider.
function gemEntryIsValid(entry) {
  if (entry.provider !== 'gem') return true;
  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
  if (!raw) return false;
  let u;
  try { u = new URL(raw); } catch { return false; }
  if (u.hostname !== 'jobs.gem.com') return false;
  return /^\/[^/?#]+/.test(u.pathname); // has a board id segment
}

Prevention

When it happens

Trigger: A portals.yml entry has provider: gem but no careers_url; careers_url points at the company's own careers site instead of jobs.gem.com; careers_url is a jobs.gem.com board but written as a deep link (e.g. https://jobs.gem.com/board/123) where the segment is still captured but if the URL is malformed/empty it returns null; the hostname has a trailing slash variant or www prefix not equal to jobs.gem.com.

Common situations: Operator adds a Gem-hosted company but copies the careers page URL (e.g. https://company.com/careers) rather than the jobs.gem.com/<boardId> URL; the entry was auto-generated with a placeholder careers_url; the Gem board URL was typed with a typo in the host.

Related errors


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