mvanhorn/last30days-skill · error · RuntimeError

EXA_API_KEY is required when web_backend='exa'

Error message

EXA_API_KEY is required when web_backend='exa'

What it means

Raised by grounding.py when the resolved web backend is 'exa' but EXA_API_KEY is absent from the config. Exa is one of the paid-key backends (brave/exa/serper/parallel); selecting it explicitly is a promise that a key exists, so a missing key is a hard RuntimeError rather than a silent empty result set. The keyless floor is only used when no paid key is configured AND web_backend was not forced.

Source

Thrown at skills/last30days/scripts/lib/grounding.py:259

            backend = "parallel"
        elif env.keyless_web_allowed(config):
            # No paid key and the host has no native search -> use the keyless
            # floor. On a native-search host this branch is skipped (the model
            # supplies web results itself), so the engine returns nothing here.
            backend = "keyless"
        else:
            return [], {}
    items: list[dict] = []
    artifact: dict = {}
    if backend == "brave":
        key = config.get("BRAVE_API_KEY")
        if not key:
            raise RuntimeError("BRAVE_API_KEY is required when web_backend='brave'")
        items, artifact = brave_search(query, date_range, key)
    elif backend == "exa":
        key = config.get("EXA_API_KEY")
        if not key:
            raise RuntimeError("EXA_API_KEY is required when web_backend='exa'")
        items, artifact = exa_search(query, date_range, key)
    elif backend == "serper":
        key = config.get("SERPER_API_KEY")
        if not key:
            raise RuntimeError("SERPER_API_KEY is required when web_backend='serper'")
        items, artifact = serper_search(query, date_range, key)
    elif backend == "parallel":
        key = config.get("PARALLEL_API_KEY")
        if not key:
            raise RuntimeError("PARALLEL_API_KEY is required when web_backend='parallel'")
        items, artifact = parallel_search(query, date_range, key)
    elif backend == "keyless":
        items, artifact = web_search_keyless.keyless_search(query, date_range, config)
    elif backend != "none":
        raise ValueError(f"Unsupported web backend: {backend!r}")
    else:
        return [], {}
    if items and not _reddit_excluded(config):

View on GitHub (pinned to c7460f6114)

Solutions

  1. Set EXA_API_KEY (Exa dashboard) in the environment or the last30days .env file and re-run.
  2. Or unset web_backend so resolution picks a backend you do have a key for, or the keyless floor.
  3. Confirm the harness/subprocess environment actually contains the variable (printenv EXA_API_KEY) before blaming the config file.

Example fix

# before
export WEB_BACKEND=exa    # EXA_API_KEY missing

# after
export EXA_API_KEY=<your-key>
export WEB_BACKEND=exa
Defensive patterns

Strategy: validation

Validate before calling

config = load_config()
if config.get("web_backend") == "exa":
    assert config.get("EXA_API_KEY"), "EXA_API_KEY must be set for exa backend"

Try / catch

try:
    grounding.web_search_with_backend(query, config)
except RuntimeError as e:
    if 'EXA_API_KEY' in str(e):
        raise SystemExit('set EXA_API_KEY or remove web_backend=exa') from e

Prevention

When it happens

Trigger: web_backend='exa' via flag/config while EXA_API_KEY is unset or empty; key stored in a project .env that is not on the load path; the variable was renamed or never obtained from Exa.

Common situations: Onboarding steps skipped: the user picked Exa as backend before completing key setup; CI has the flag in config but secrets are injected under a different name; switching backends without switching keys.

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/3b179c55101ff2f5. Report an issue: GitHub.