santifer/career-ops · error · Error

gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET /…

Error message

gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in .env

What it means

The gmail plugin's ingest() requires three OAuth credentials from the environment: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, and GMAIL_REFRESH_TOKEN. If any is missing or empty, it throws immediately with a message naming all three.

Solutions

  1. Add all three: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN to .env.
  2. If you lack a refresh token, complete the OAuth consent flow (e.g. OAuth 2.0 playground with your client id/secret, scope https://www.googleapis.com/auth/gmail.readonly) and store the refresh token.
  3. Ensure your runner actually loads .env and the variable names match exactly.
  4. Restart the process after editing .env.

Example fix

// before
GMAIL_CLIENT_ID=xxx.apps.googleusercontent.com
# missing secret and refresh token
// after
GMAIL_CLIENT_ID=xxx.apps.googleusercontent.com
GMAIL_CLIENT_SECRET=xxxxxxxx
GMAIL_REFRESH_TOKEN=1//xxxxxxxx
Defensive patterns

Strategy: validation

Validate before calling

const required = ['GMAIL_CLIENT_ID','GMAIL_CLIENT_SECRET','GMAIL_REFRESH_TOKEN'];
const missing = required.filter(k => !process.env[k]);
if (missing.length) throw new Error(`missing Gmail env vars: ${missing.join(', ')}`);

Try / catch

try {
  await gmailPlugin.ingest(ctx);
} catch (e) {
  if (String(e.message).includes('missing GMAIL_CLIENT_ID')) {
    console.error('Add GMAIL_CLIENT_ID/SECRET/REFRESH_TOKEN to .env');
    return [];
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling ingest (gmail ingestion during scan/pipeline) when any of the three env vars is unset — typically a partially filled .env with only some credentials, or none at all.

Common situations: Setting up Gmail ingestion and copying only client id/secret but never completing the OAuth flow for a refresh token; .env not loaded in the runtime (docker/CI); variable name typos (GMAIL_REFRESH_TOKEN vs GMAIL_REFRESH_TOKEN_).

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


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/5660e633ace70ba5. Report an issue: GitHub.

Appendix: source

Thrown at plugins/gmail/index.mjs:77

}

function saveProcessedIds(ids) {
  try {
    mkdirSync('data', { recursive: true });
    writeFileSync(STATE_PATH, JSON.stringify({ processed_message_ids: [...ids] }, null, 2), 'utf-8');
  } catch (err) {
    console.warn(`gmail: could not persist processed-id state — ${err.message}`);
  }
}

/** @type {{ ingest: (ctx: any) => Promise<object[]> }} */
export default {
  async ingest(ctx) {
    const clientId = ctx?.env?.GMAIL_CLIENT_ID;
    const clientSecret = ctx?.env?.GMAIL_CLIENT_SECRET;
    const refreshToken = ctx?.env?.GMAIL_REFRESH_TOKEN;
    if (!clientId || !clientSecret || !refreshToken) {
      throw new Error('gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL_REFRESH_TOKEN in .env');
    }

    const label = ctx?.settings?.label || 'Job Leads';
    const daysBack = Number(ctx?.settings?.days_back ?? 7);
    if (!Number.isInteger(daysBack) || daysBack <= 0) {
      throw new Error(`gmail: invalid days_back "${ctx?.settings?.days_back}" (must be a positive integer)`);
    }

    const token = await getAccessToken({ clientId, clientSecret, refreshToken }, ctx.fetch);
    const auth = { Authorization: `Bearer ${token}` };
    const query = `label:"${label}" newer_than:${daysBack}d`;
    ctx.log(`gmail: querying ${query}`);

    // List message ids (paginated). ctx.fetch throws on a non-2xx (with the body
    // in the message), so a failed page surfaces a clear error.
    const messages = [];
    let pageToken = null;
    do {

View on GitHub (pinned to aac998c7ed)