santifer/career-ops · error
csod: no anonymous token on
Error message
csod: no anonymous token on ${cfg.homeUrl} What it means
The Cornerstone OnDemand (CSOD) provider bootstraps each fetch by GETting the tenant's career-site home page, which embeds an anonymous bearer JWT ("token":"eyJ…") plus session cookies needed by the public search API. When `extractToken(html)` finds no token in that bootstrap HTML, the provider throws because the subsequent POST to /services/x/career-site/v1/search cannot authenticate. This almost always means the fetched page is not the expected ~5 KB bootstrap document.
Solutions
- Fix the portal entry so `api:` (or careers_url) points at the real *.csod.com URL matching /ux/ats/careersite/\d+/home?c=<corpName>, not the branded corporate page.
- Open cfg.homeUrl in a browser and inspect the page source for "token":"eyJ — if absent, the tenant gates the board; remove or replace the entry.
- Verify the siteId and c= corpName parameters are correct (wrong values land on error/redirect pages without a token).
- Check the provider version against the tenant's current page markup — if CSOD renamed the embedded token field, extractToken must be updated.
- If a redirect is silently followed to a login page, ensure the fetch keeps redirect:'error' semantics so the misconfiguration surfaces before token extraction.
Example fix
// before (portals.yml) - name: ohb careers_url: https://www.ohb.de/karriere // after - name: ohb careers_url: https://www.ohb.de/karriere api: https://career-ohb.csod.com/ux/ats/careersite/4/home?c=career-ohb
Defensive patterns
Strategy: validation
Validate before calling
// validate the entry before scanning
const u = new URL(entry.api || entry.careers_url);
if (!(u.protocol === 'https:' && (u.host === 'csod.com' || u.host.endsWith('.csod.com')) &&
/\/ux\/ats\/careersite\/\d+\//.test(u.pathname))) {
throw new Error('entry must point at https://<tenant>.csod.com/ux/ats/careersite/<id>/home?c=<corp>');
} Try / catch
try {
await provider.fetch(entry, ctx);
} catch (e) {
if (e.message.startsWith('csod: no anonymous token')) {
// board gated or wrong URL — flag entry for manual review, don't retry
reportUnreachable(entry, e.message);
} else throw e;
} Prevention
- Always set `api:` to the raw *.csod.com careersite URL, keeping the branded page in careers_url only.
- Verify the careersite path shape /ux/ats/careersite/{id}/home?c={corpName} when adding a new CSOD tenant.
- Open the homeUrl in a browser and confirm an anonymous session (no login wall) sees job listings.
- Re-check tenants periodically: consent interstitials and CSOD upgrades can remove the embedded token.
When it happens
Trigger: fetch() completed the GET of cfg.homeUrl (either via ctx.fetch with cookies, or ctx.fetchText with redirect:'error') and extractToken() on the response body returned falsy — i.e. the HTML contains no embedded `"token":"…"` anonymous JWT. Happens when the homeUrl points at the branded corporate careers page instead of the *.csod.com careersite URL, when the site redirects to a login/consent/captcha page, or when the tenant has changed its page structure so the token is no longer inline.
Common situations: Portals.yml entry where `careers_url` is the company's branded careers page and no `api:` field carries the https://{tenant}.csod.com/ux/ats/careersite/{siteId}/home?c={corpName} URL; a tenant that now gates anonymous access behind a cookie-consent interstitial; a wrong siteId or corpName query param landing on an error page; CSOD changing the bootstrap format in an upgrade.
Related errors
- jobvite: could not find companyEId on
- vc-portfolios: a16z portfolio fetch failed
- Access denied: Egress guard blocked private target IP
- Access denied: Egress guard blocked private target IP
- All active models failed. Last error
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/c3ef00235e940af1.
Report an issue: GitHub.
Appendix: source
Thrown at providers/csod.mjs:207
// it (older embedders and test mocks), which keeps the pre-cookie
// behaviour intact for tenants that never needed it.
//
// cfg.homeUrl and cfg.searchApi are both built from the same parsed
// origin, so replaying these cookies cannot reach a third-party host.
// redirect:'error' on the bootstrap keeps that true: origin validation
// covers the URL we ask for, not wherever a 3xx would send us.
let html;
let cookie = '';
if (typeof ctx.fetchResponse === 'function') {
const res = await ctx.fetchResponse(cfg.homeUrl, { redirect: 'error', headers: { accept: 'text/html' } });
const setCookies = typeof res?.headers?.getSetCookie === 'function' ? res.headers.getSetCookie() : [];
cookie = cookieHeaderFrom(setCookies);
html = await res.text();
} else {
html = await ctx.fetchText(cfg.homeUrl, { redirect: 'error', headers: { accept: 'text/html' } });
}
const token = extractToken(html);
if (!token) throw new Error(`csod: no anonymous token on ${cfg.homeUrl}`);
const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));
const maxPages = resolveMaxPages(entry);
const jobs = [];
const seen = new Set();
let total = null;
for (let page = 1; page <= maxPages; page++) {
if (page > 1) await wait(PAGE_DELAY_MS);
const json = await ctx.fetchJson(cfg.searchApi, {
method: 'POST',
redirect: 'error',
headers: {
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${token}`,
...(cookie ? { cookie } : {}),
},View on GitHub (pinned to aac998c7ed)