{"record":{"id":"1e4fdef6455257c2","repo":"santifer/career-ops","slug":"gem-cannot-derive-board-id-for-entry-name","errorCode":null,"errorMessage":"gem: cannot derive board id for ${entry.name}","messagePattern":"gem: cannot derive board id for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/gem.mjs","lineNumber":199,"sourceCode":"  detect(entry) {\n    const restApiUrl = resolveRestApiUrl(entry);\n    if (restApiUrl) return { url: restApiUrl.href };\n    const boardId = resolveBoardId(entry);\n    return boardId ? { url: `${GEM_API_URL}?board=${boardId}` } : null;\n  },\n\n  async fetch(entry, ctx) {\n    // Gem documents this unauthenticated REST surface for custom career pages.\n    // Keep the existing GraphQL path for jobs.gem.com SPA boards, while\n    // allowing operators to pin a verified REST URL from a captured page.\n    const restApiUrl = resolveRestApiUrl(entry);\n    if (restApiUrl) {\n      assertGemUrl(restApiUrl.href);\n      const json = /** @type {any} */ (await ctx.fetchJson(restApiUrl.href, { redirect: 'error' }));\n      return parseRestResponse(json, entry.name);\n    }\n    const boardId = resolveBoardId(entry);\n    if (!boardId) throw new Error(`gem: cannot derive board id for ${entry.name}`);\n    assertGemUrl(GEM_API_URL);\n\n    const body = JSON.stringify([\n      { operationName: 'JobBoardList', variables: { boardId }, query: JOB_BOARD_LIST_QUERY },\n    ]);\n    // redirect:'error' prevents SSRF via server-side redirects; combined with\n    // assertGemUrl above it guarantees the final hostname stays in the allowlist.\n    const json = /** @type {any} */ (await ctx.fetchJson(GEM_API_URL, {\n      method: 'POST',\n      headers: { 'content-type': 'application/json', batch: 'true' },\n      body,\n      redirect: 'error',\n    }));\n\n    const listResult = json?.[0];\n    if (Array.isArray(listResult?.errors) && listResult.errors.length > 0) {\n      throw new Error(`gem: JobBoardList failed: ${listResult.errors[0]?.message || 'unknown GraphQL error'}`);\n    }","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/gem.mjs#L181-L217","documentation":"The Gem provider has two fetch paths: an explicit REST API URL from the board entry, or a GraphQL JobBoardList call keyed by a board id. When neither an api URL nor a derivable board id exists for the entry, fetch cannot build a request and throws this error naming the entry.","triggerScenarios":"A portals.yml Gem entry that has neither an api URL (or resolveRestApiUrl returned empty) nor any field from which resolveBoardId can extract a board id (e.g. board URL path missing the id segment, or the entry only has a generic careers-page URL).","commonSituations":"Adding a Gem company by pasting only its careers-site URL rather than its Gem board URL; Gem changing their board URL format so the id-extraction logic no longer matches; a partially migrated config entry.","solutions":["Add the board id explicitly to the entry (or the full Gem board URL containing it) so resolveBoardId can extract it.","Configure the entry's api field with Gem's documented REST endpoint URL.","Check the Gem board URL format against what resolveBoardId expects — Gem may have changed their slug/id layout.","Verify the entry is actually a Gem-hosted board; non-Gem boards need a different provider."],"exampleFix":"// before\n{ name: 'acme', careers_url: 'https://acme.com/careers' }\n// after\n{ name: 'acme', careers_url: 'https://acme.com/careers', api: 'https://api.gem.com/v1/boards/acme-123/job_posts' }","handlingStrategy":"validation","validationCode":"if (!entry.api && !resolveBoardId(entry)) throw new Error(`Gem entry '${entry.name}' needs an api URL or a board id`);","typeGuard":"const hasGemTarget = (entry) => Boolean(typeof entry.api === 'string' && entry.api) || Boolean(resolveBoardId(entry));","tryCatchPattern":"try { await provider.fetch(ctx, entry); } catch (e) { if (e.message.includes('cannot derive board id')) console.error(`Fix entry '${entry.name}': add its Gem board URL or api field`); throw e; }","preventionTips":["Always configure the board id or api URL when adding a Gem company","Validate all entries at startup with a config linter","Copy the board URL from Gem itself, not the company's careers page","Re-check entries after Gem UI/URL changes"],"tags":["config","provider","missing-field"],"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"}