/** * Ashby public job board API. * * https://developers.ashbyhq.com/reference/posting-api-job-board * GET https://api.ashbyhq.com/posting-api/job-board/{company} * * No auth. Each entry in `ashbyCompanies` is fetched independently. */ import type { ExtractorManifest, ExtractorRunResult, } from "@shared/types/extractors"; import type { CreateJobInput } from "@shared/types/jobs"; interface AshbyAddress { postalAddress?: { addressLocality?: string; addressRegion?: string; addressCountry?: string; }; } interface AshbyJob { id?: string; title?: string; jobUrl?: string; applyUrl?: string; publishedAt?: string; employmentType?: string; isRemote?: boolean; team?: string; department?: string; location?: string; locationName?: string; secondaryLocations?: Array<{ location?: string; locationName?: string }>; address?: AshbyAddress; descriptionPlain?: string; descriptionHtml?: string; } interface AshbyResponse { jobs?: AshbyJob[]; apiVersion?: string; } function asString(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed ? trimmed : undefined; } function readCompanies(raw: string | undefined): string[] { if (!raw) return []; try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { return parsed .map((entry) => (typeof entry === "string" ? entry.trim() : "")) .filter(Boolean); } } catch { // fall through } return raw .split(/[\n,;|]+/) .map((entry) => entry.trim()) .filter(Boolean); } function locationFor(job: AshbyJob): string | undefined { const primary = asString(job.locationName) ?? asString(job.location) ?? undefined; const secondary = job.secondaryLocations ?.map((entry) => asString(entry.locationName) ?? asString(entry.location)) .filter((value): value is string => Boolean(value)) ?? []; const all = [primary, ...secondary].filter((value): value is string => Boolean(value), ); return all.length > 0 ? all.join("; ") : undefined; } function mapJob(job: AshbyJob, company: string): CreateJobInput | null { const jobUrl = asString(job.jobUrl) ?? asString(job.applyUrl); if (!jobUrl) return null; const employer = company .split(/[-_]/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); return { source: "ashby", sourceJobId: asString(job.id), title: asString(job.title) ?? "Unknown Title", employer: employer || company, jobUrl, applicationLink: asString(job.applyUrl) ?? jobUrl, location: locationFor(job), isRemote: typeof job.isRemote === "boolean" ? job.isRemote : undefined, jobType: asString(job.employmentType), jobFunction: asString(job.team), companyIndustry: asString(job.department), datePosted: asString(job.publishedAt), jobDescription: asString(job.descriptionPlain) ?? asString(job.descriptionHtml), }; } async function fetchCompany(company: string): Promise { const url = `https://api.ashbyhq.com/posting-api/job-board/${encodeURIComponent(company)}`; const response = await fetch(url, { headers: { Accept: "application/json" }, }); if (response.status === 404) return []; if (!response.ok) { throw new Error( `Ashby request for "${company}" failed with status ${response.status}`, ); } const body = (await response.json()) as AshbyResponse; return Array.isArray(body.jobs) ? body.jobs : []; } export const manifest: ExtractorManifest = { id: "ashby", displayName: "Ashby (ATS)", providesSources: ["ashby"], async run(context): Promise { if (context.shouldCancel?.()) return { success: true, jobs: [] }; const companies = readCompanies(context.settings.ashbyCompanies); if (companies.length === 0) { return { success: true, jobs: [], error: "No Ashby companies configured. Set ASHBY_COMPANIES or the ashbyCompanies setting (comma- or newline-separated slugs).", }; } const seen = new Set(); const out: CreateJobInput[] = []; try { for (let i = 0; i < companies.length; i += 1) { if (context.shouldCancel?.()) break; const company = companies[i]; context.onProgress?.({ phase: "list", termsProcessed: i, termsTotal: companies.length, currentUrl: company, detail: `Ashby: ${company} (${i + 1}/${companies.length})`, }); let added = 0; const jobs = await fetchCompany(company); for (const job of jobs) { const mapped = mapJob(job, company); if (!mapped) continue; const key = mapped.sourceJobId || mapped.jobUrl; if (seen.has(key)) continue; seen.add(key); out.push(mapped); added += 1; } context.onProgress?.({ phase: "list", termsProcessed: i + 1, termsTotal: companies.length, currentUrl: company, jobPagesProcessed: out.length, detail: `Ashby: ${company} → ${added} jobs (${out.length} total)`, }); } } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { success: false, jobs: out, error: message }; } return { success: true, jobs: out }; }, }; export default manifest;