santifer/career-ops · error · Error
NOTION_ACCESS_TOKEN is not set (.env) — the Notion plugin ne
Error message
NOTION_ACCESS_TOKEN is not set (.env) — the Notion plugin needs it to read/write.
What it means
The Notion plugin helper createNotionClient() builds a scoped API client bound to one user's integration token. A token is mandatory because every Notion API call is authenticated with `Bearer <token>`. This guard throws before any network call when the token argument is falsy, so it fails fast rather than emitting 401s downstream.
Source
Thrown at plugins/notion/_notion.mjs:67
const str = String(text ?? '');
const out = [];
for (let i = 0; i < str.length || out.length === 0; i += MAX) out.push({ type: 'text', text: { content: str.slice(i, i + MAX) } });
return out;
}
export function plain(prop) {
return (prop?.title || prop?.rich_text || []).map((t) => t.plain_text).join('');
}
/**
* Build a Notion client bound to one user's token + parent page. Network goes
* through the injected `fetchFn` (the plugin passes ctx.fetch so the engine's
* allowedHosts/HTTPS/redirect guard applies); falls back to global fetch for
* standalone use. Nothing here reads process.env.
* @param {{ token: string, parent: string, fetch?: Function }} cfg
*/
export function createNotionClient({ token, parent, fetch: fetchFn = globalThis.fetch }) {
if (!token) throw new Error('NOTION_ACCESS_TOKEN is not set (.env) — the Notion plugin needs it to read/write.');
const HEADERS = { Authorization: `Bearer ${token}`, 'Notion-Version': '2025-09-03', 'Content-Type': 'application/json' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function api(path, method, body) {
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);View on GitHub (pinned to 9b17a8ac97)
Solutions
- Create an internal integration at https://www.notion.so/profile/integrations and copy the Internal Integration Secret.
- Add `NOTION_ACCESS_TOKEN=<secret>` to .env (the token starts with `ntn_` or `secret_`).
- Share the target Notion page(s) with the integration (Page menu → Connect to → your integration).
- Re-run `node plugins.mjs run notion`.
Example fix
# before (.env) # (no Notion vars) # after (.env) NOTION_ACCESS_TOKEN=ntn_xxxxxxxxxxxxxxxxxxxxxxxxxxxx NOTION_PARENT_PAGE_ID=abc123def456
Defensive patterns
Strategy: validation
Validate before calling
// Guard the token before constructing the client.
function notionClientFromCtx(ctx) {
const token = ctx?.env?.NOTION_ACCESS_TOKEN;
if (!token) {
throw new Error('Configure NOTION_ACCESS_TOKEN in .env before using the Notion plugin.');
}
return createNotionClient({ token, parent: ctx?.env?.NOTION_PARENT_PAGE_ID, fetch: ctx.fetch });
} Type guard
/** @param {unknown} t @returns {t is string} */
function isNonEmptyToken(t) {
return typeof t === 'string' && t.trim().length > 0;
} Try / catch
try {
const client = createNotionClient({ token, parent });
} catch (err) {
if (err instanceof Error && err.message.includes('NOTION_ACCESS_TOKEN')) {
console.error('Notion disabled — set NOTION_ACCESS_TOKEN in .env.');
} else throw err;
} Prevention
- Centralize a `requiredEnv(['NOTION_ACCESS_TOKEN', 'NOTION_PARENT_PAGE_ID'])` helper that fails at startup with a clear message.
- Treat an absent token as a disabled plugin (skip gracefully) rather than throwing mid-operation.
When it happens
Trigger: Calling createNotionClient({token, ...}) — directly or via plugins/notion/index.mjs's clientFromCtx() — when NOTION_ACCESS_TOKEN is absent from .env (ctx.env.NOTION_ACCESS_TOKEN is undefined).
Common situations: Notion integration not created yet in Notion (Settings → My connections → Develop your own integration); integration created but its secret never copied into .env; .env var name typoed (e.g. NOTION_TOKEN instead of NOTION_ACCESS_TOKEN).
Related errors
- gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL
- Notion ${method} ${path} -> ${j.code}: ${j.message}
- Set NOTION_PARENT_PAGE_ID in .env (the "Career Ops" parent p
- APIFY_TOKEN not set — enable apify in config/plugins.yml and
- No "Applications" database found under the Career Ops page —
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/48bf0f7f469dca0e.
Report an issue: GitHub.