santifer/career-ops · error
gem: cannot derive board id for
Error message
gem: cannot derive board id for ${entry.name} What it means
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.
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.
Example fix
// before
{ name: 'acme', careers_url: 'https://acme.com/careers' }
// after
{ name: 'acme', careers_url: 'https://acme.com/careers', api: 'https://api.gem.com/v1/boards/acme-123/job_posts' } Defensive patterns
Strategy: validation
Validate before calling
if (!entry.api && !resolveBoardId(entry)) throw new Error(`Gem entry '${entry.name}' needs an api URL or a board id`); Type guard
const hasGemTarget = (entry) => Boolean(typeof entry.api === 'string' && entry.api) || Boolean(resolveBoardId(entry));
Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
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
- arbeitsagentur: entry
- breezy: cannot derive API URL for
- jibeapply: careers_url required
- rippling: cannot derive API URL for
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/1e4fdef6455257c2.
Report an issue: GitHub.
Appendix: source
Thrown at providers/gem.mjs:199
detect(entry) {
const restApiUrl = resolveRestApiUrl(entry);
if (restApiUrl) return { url: restApiUrl.href };
const boardId = resolveBoardId(entry);
return boardId ? { url: `${GEM_API_URL}?board=${boardId}` } : null;
},
async fetch(entry, ctx) {
// Gem documents this unauthenticated REST surface for custom career pages.
// Keep the existing GraphQL path for jobs.gem.com SPA boards, while
// allowing operators to pin a verified REST URL from a captured page.
const restApiUrl = resolveRestApiUrl(entry);
if (restApiUrl) {
assertGemUrl(restApiUrl.href);
const json = /** @type {any} */ (await ctx.fetchJson(restApiUrl.href, { redirect: 'error' }));
return parseRestResponse(json, entry.name);
}
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 aac998c7ed)