santifer/career-ops · critical · Error
Set NOTION_PARENT_PAGE_ID in .env (the "Career Ops" parent…
Error message
Set NOTION_PARENT_PAGE_ID in .env (the "Career Ops" parent page id).
What it means
resolveDBs() enumerates the child databases under a configured Notion parent page to map each database name to its primary data source id. It requires the parent page id (taken from NOTION_PARENT_PAGE_ID and passed into the client as cfg.parent) to be set; without it there is nothing to enumerate, so it throws immediately with instructions to set the variable in .env.
Solutions
- Add NOTION_PARENT_PAGE_ID to .env with the 32-hex-char id of the 'Career Ops' parent page (the id segment of the page URL).
- Ensure the parent page is shared with your Notion integration (page → … menu → Connections → add the integration), otherwise the id resolves but enumeration returns nothing.
- Verify createNotionClient is called with { token, parent } and that the parent value is non-empty after trim.
- In CI, add the NOTION_PARENT_PAGE_ID secret alongside the token so both are injected.
Example fix
// before (.env) NOTION_ACCESS_TOKEN=secret_xxx # NOTION_PARENT_PAGE_ID not set // after (.env) NOTION_ACCESS_TOKEN=secret_xxx NOTION_PARENT_PAGE_ID=1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
Defensive patterns
Strategy: validation
Validate before calling
// before constructing the client
const parent = (process.env.NOTION_PARENT_PAGE_ID || '').trim();
if (!parent) {
throw new Error('NOTION_PARENT_PAGE_ID missing in .env — paste the id of the Career Ops parent page');
} Type guard
function hasNotionParent(env = process.env): env is typeof env & { NOTION_PARENT_PAGE_ID: string } {
return typeof env.NOTION_PARENT_PAGE_ID === 'string' && /^[0-9a-f]{32}$/i.test(env.NOTION_PARENT_PAGE_ID.trim());
} Try / catch
try {
const dbs = await client.resolveDBs();
} catch (e) {
if (/NOTION_PARENT_PAGE_ID/.test(e.message)) {
console.error('Add NOTION_PARENT_PAGE_ID=<32-hex page id> to .env, then share that page with your integration.');
process.exit(1);
}
throw e;
} Prevention
- Copy the parent page id from the Notion page URL into .env during initial setup, next to the token.
- Check both Notion vars together in one startup validation step (token + parent).
- Remember that setting the id is not enough — the page must also be connected to the integration.
- Re-verify the id after moving or duplicating the parent page in Notion.
When it happens
Trigger: Calling resolveDBs() (directly or via any plugin operation that needs to locate the Career Ops databases) when NOTION_PARENT_PAGE_ID is missing from .env, the cfg object passed to createNotionClient omitted parent, or the variable is present but empty so it falsy-coerces to the same branch.
Common situations: Fresh setup where the user copied the integration token into .env but not the parent page id; the parent page was moved/duplicated and the old id was removed from .env; the id was pasted with the Notion URL's trailing hash formatting stripped incorrectly (though an empty value is the case that throws here); CI runs where only the token secret was configured.
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
Related errors
- NOTION_ACCESS_TOKEN is not set (.env) — the Notion plugin…
- H1B_INDEX_PATH is set but empty. Unset it to use the…
- No "Applications" database found under the Career Ops page…
- OPENROUTER_API_KEY not found. Copy .env.example to .env and…
- a16z-speedrun-talent: invalid URL
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/0fa31d7151051a93.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/notion/_notion.mjs:90
await sleep(360); // ~3 req/s
// ctx.fetch throws on non-2xx (its message carries the body); the !r.ok
// branch below is the fallback when a plain global fetch is injected.
const r = await fetchFn(`https://api.notion.com/v1/${path}`, { method, headers: HEADERS, body: body ? JSON.stringify(body) : undefined });
const j = await r.json();
if (!r.ok) throw new Error(`Notion ${method} ${path} -> ${j.code}: ${j.message}`);
return j;
}
/** Create a page in a data source. `markdown` (optional) becomes the page body. */
async function createPage(dataSourceId, properties, markdown) {
const body = { parent: { type: 'data_source_id', data_source_id: dataSourceId }, properties };
if (markdown) body.markdown = markdown;
return api('pages', 'POST', body);
}
/** Map of DB name → primary data source id for every DB under the parent page. */
async function resolveDBs() {
if (!parent) throw new Error('Set NOTION_PARENT_PAGE_ID in .env (the "Career Ops" parent page id).');
const out = {};
let cursor;
do {
const j = await api(`blocks/${parent}/children?page_size=100${cursor ? `&start_cursor=${cursor}` : ''}`, 'GET');
for (const b of j.results) {
if (b.type !== 'child_database') continue;
const db = await api(`databases/${b.id}`, 'GET');
out[b.child_database.title] = db.data_sources?.[0]?.id;
}
cursor = j.has_more ? j.next_cursor : null;
} while (cursor);
return out;
}
async function queryDB(dataSourceId) {
let cursor, all = [];
do {
const j = await api(`data_sources/${dataSourceId}/query`, 'POST', { page_size: 100, start_cursor: cursor });View on GitHub (pinned to aac998c7ed)