Files
simstudioai--sim/apps/sim/tools/ashby/utils.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

914 lines
34 KiB
TypeScript

import type {
AshbyApplication,
AshbyCandidate,
AshbyContactInfo,
AshbyCustomField,
AshbyFileHandle,
AshbyHiringTeamMember,
AshbyJob,
AshbyOffer,
AshbyOfferVersion,
AshbyOpening,
AshbyOpeningLatestVersion,
AshbySourceSummary,
AshbyUserSummary,
} from '@/tools/ashby/types'
import type { OutputProperty } from '@/tools/types'
type Unknown = Record<string, unknown>
/**
* Build the standard Ashby Authorization header. Ashby uses HTTP Basic auth
* with the API key as the username and an empty password.
*/
export function ashbyAuthHeaders(apiKey: string): Record<string, string> {
return {
'Content-Type': 'application/json',
Accept: 'application/json; version=1',
Authorization: `Basic ${btoa(`${apiKey}:`)}`,
}
}
/**
* Extract a human-readable error message from an Ashby error response. Ashby
* returns errors as either `errorInfo.message` or an `errors` string array.
*/
export function ashbyErrorMessage(data: unknown, fallback: string): string {
if (!data || typeof data !== 'object') return fallback
const d = data as Unknown
const info = d.errorInfo as Unknown | undefined
if (info && typeof info.message === 'string' && info.message) return info.message
if (Array.isArray(d.errors) && d.errors.length > 0) {
return d.errors.map((e) => String(e)).join('; ')
}
return fallback
}
function mapContact(raw: unknown): AshbyContactInfo | null {
if (!raw || typeof raw !== 'object') return null
const c = raw as Unknown
return {
value: (c.value as string) ?? '',
type: (c.type as string) ?? 'Other',
isPrimary: (c.isPrimary as boolean) ?? true,
}
}
function mapContactArray(raw: unknown): AshbyContactInfo[] {
if (!Array.isArray(raw)) return []
return raw.map((c) => mapContact(c)).filter((c): c is AshbyContactInfo => c !== null)
}
function mapCustomFields(raw: unknown): AshbyCustomField[] {
if (!Array.isArray(raw)) return []
return raw.map((f) => {
const cf = f as Unknown
return {
id: (cf.id as string) ?? null,
title: (cf.title as string) ?? '',
isPrivate: (cf.isPrivate as boolean) ?? false,
valueLabel: (cf.valueLabel as string) ?? null,
value: cf.value ?? null,
}
})
}
function mapFileHandle(raw: unknown): AshbyFileHandle | null {
if (!raw || typeof raw !== 'object') return null
const f = raw as Unknown
return {
id: (f.id as string) ?? '',
name: (f.name as string) ?? '',
handle: (f.handle as string) ?? '',
}
}
function mapFileHandles(raw: unknown): AshbyFileHandle[] {
if (!Array.isArray(raw)) return []
return raw.map((f) => mapFileHandle(f)).filter((f): f is AshbyFileHandle => f !== null)
}
export function mapUserSummary(raw: unknown): AshbyUserSummary | null {
if (!raw || typeof raw !== 'object') return null
const u = raw as Unknown
return {
id: (u.id as string) ?? '',
firstName: (u.firstName as string) ?? null,
lastName: (u.lastName as string) ?? null,
email: (u.email as string) ?? null,
globalRole: (u.globalRole as string) ?? null,
isEnabled: (u.isEnabled as boolean) ?? false,
updatedAt: (u.updatedAt as string) ?? null,
managerId: (u.managerId as string) ?? null,
}
}
function mapSource(raw: unknown): AshbySourceSummary | null {
if (!raw || typeof raw !== 'object') return null
const s = raw as Unknown
const sourceType = s.sourceType as Unknown | undefined
return {
id: (s.id as string) ?? '',
title: (s.title as string) ?? '',
isArchived: (s.isArchived as boolean) ?? false,
sourceType: sourceType
? {
id: (sourceType.id as string) ?? '',
title: (sourceType.title as string) ?? '',
isArchived: (sourceType.isArchived as boolean) ?? false,
}
: null,
}
}
export function mapCandidate(raw: unknown): AshbyCandidate {
const c = (raw ?? {}) as Unknown
const socialLinks = Array.isArray(c.socialLinks)
? (c.socialLinks as Array<{ type?: string; url?: string }>)
: []
const location = c.location as Unknown | undefined
const locationComponents = Array.isArray(location?.locationComponents)
? (location?.locationComponents as Array<{ type?: string; name?: string }>)
: []
return {
id: (c.id as string) ?? '',
name: (c.name as string) ?? '',
primaryEmailAddress: mapContact(c.primaryEmailAddress),
primaryPhoneNumber: mapContact(c.primaryPhoneNumber),
emailAddresses: mapContactArray(c.emailAddresses),
phoneNumbers: mapContactArray(c.phoneNumbers),
socialLinks: socialLinks.map((l) => ({
type: l.type ?? '',
url: l.url ?? '',
})),
linkedInUrl: socialLinks.find((l) => l.type === 'LinkedIn')?.url ?? null,
githubUrl: socialLinks.find((l) => l.type === 'GitHub')?.url ?? null,
profileUrl: (c.profileUrl as string) ?? null,
position: (c.position as string) ?? null,
company: (c.company as string) ?? null,
school: (c.school as string) ?? null,
timezone: (c.timezone as string) ?? null,
location: location
? {
id: (location.id as string) ?? null,
locationSummary: (location.locationSummary as string) ?? null,
locationComponents: locationComponents.map((lc) => ({
type: lc.type ?? '',
name: lc.name ?? '',
})),
}
: null,
tags: Array.isArray(c.tags)
? (c.tags as Array<{ id?: string; title?: string; isArchived?: boolean }>).map((t) => ({
id: t.id ?? '',
title: t.title ?? '',
isArchived: t.isArchived ?? false,
}))
: [],
applicationIds: Array.isArray(c.applicationIds) ? (c.applicationIds as string[]) : [],
customFields: mapCustomFields(c.customFields),
resumeFileHandle: mapFileHandle(c.resumeFileHandle),
fileHandles: mapFileHandles(c.fileHandles),
source: mapSource(c.source),
creditedToUser: mapUserSummary(c.creditedToUser),
fraudStatus: (c.fraudStatus as string) ?? null,
createdAt: (c.createdAt as string) ?? null,
updatedAt: (c.updatedAt as string) ?? null,
}
}
function mapHiringTeam(raw: unknown): AshbyHiringTeamMember[] {
if (!Array.isArray(raw)) return []
return raw.map((m) => {
const mem = m as Unknown
return {
email: (mem.email as string) ?? null,
firstName: (mem.firstName as string) ?? null,
lastName: (mem.lastName as string) ?? null,
role: (mem.role as string) ?? null,
userId: (mem.userId as string) ?? null,
}
})
}
function mapOpeningLatestVersion(raw: unknown): AshbyOpeningLatestVersion | null {
if (!raw || typeof raw !== 'object') return null
const v = raw as Unknown
return {
id: (v.id as string) ?? null,
identifier: (v.identifier as string) ?? null,
description: (v.description as string) ?? null,
authorId: (v.authorId as string) ?? null,
createdAt: (v.createdAt as string) ?? null,
teamId: (v.teamId as string) ?? null,
jobIds: Array.isArray(v.jobIds) ? (v.jobIds as string[]) : [],
targetHireDate: (v.targetHireDate as string) ?? null,
targetStartDate: (v.targetStartDate as string) ?? null,
isBackfill: (v.isBackfill as boolean) ?? false,
employmentType: (v.employmentType as string) ?? null,
locationIds: Array.isArray(v.locationIds) ? (v.locationIds as string[]) : [],
hiringTeam: mapHiringTeam(v.hiringTeam),
customFields: mapCustomFields(v.customFields),
}
}
export function mapOpenings(raw: unknown): AshbyOpening[] {
if (!Array.isArray(raw)) return []
return raw.map((o) => {
const op = o as Unknown
return {
id: (op.id as string) ?? '',
openedAt: (op.openedAt as string) ?? null,
closedAt: (op.closedAt as string) ?? null,
isArchived: (op.isArchived as boolean) ?? false,
archivedAt: (op.archivedAt as string) ?? null,
closeReasonId: (op.closeReasonId as string) ?? null,
openingState: (op.openingState as string) ?? null,
latestVersion: mapOpeningLatestVersion(op.latestVersion),
}
})
}
export function mapJob(raw: unknown): AshbyJob {
const j = (raw ?? {}) as Unknown
const location = j.location as Unknown | undefined
const address = location?.address as Unknown | undefined
const postalAddress = address?.postalAddress as Unknown | undefined
const compensation = j.compensation as Unknown | undefined
return {
id: (j.id as string) ?? '',
title: (j.title as string) ?? '',
confidential: (j.confidential as boolean) ?? false,
status: (j.status as string) ?? null,
employmentType: (j.employmentType as string) ?? null,
locationId: (j.locationId as string) ?? null,
departmentId: (j.departmentId as string) ?? null,
defaultInterviewPlanId: (j.defaultInterviewPlanId as string) ?? null,
interviewPlanIds: Array.isArray(j.interviewPlanIds) ? (j.interviewPlanIds as string[]) : [],
customFields: mapCustomFields(j.customFields),
jobPostingIds: Array.isArray(j.jobPostingIds) ? (j.jobPostingIds as string[]) : [],
customRequisitionId: (j.customRequisitionId as string) ?? null,
brandId: (j.brandId as string) ?? null,
hiringTeam: mapHiringTeam(j.hiringTeam),
author: mapUserSummary(j.author),
createdAt: (j.createdAt as string) ?? null,
updatedAt: (j.updatedAt as string) ?? null,
openedAt: (j.openedAt as string) ?? null,
closedAt: (j.closedAt as string) ?? null,
location: location
? {
id: (location.id as string) ?? null,
name: (location.name as string) ?? null,
externalName: (location.externalName as string) ?? null,
isArchived: (location.isArchived as boolean) ?? false,
isRemote: (location.isRemote as boolean) ?? false,
workplaceType: (location.workplaceType as string) ?? null,
parentLocationId: (location.parentLocationId as string) ?? null,
type: (location.type as string) ?? null,
address: postalAddress
? {
addressCountry: (postalAddress.addressCountry as string) ?? null,
addressRegion: (postalAddress.addressRegion as string) ?? null,
addressLocality: (postalAddress.addressLocality as string) ?? null,
postalCode: (postalAddress.postalCode as string) ?? null,
streetAddress: (postalAddress.streetAddress as string) ?? null,
}
: null,
}
: null,
openings: mapOpenings(j.openings),
compensation: compensation
? {
compensationTiers: Array.isArray(compensation.compensationTiers)
? (
compensation.compensationTiers as Array<{
id?: string
title?: string
additionalInformation?: string
tierSummary?: string
}>
).map((t) => ({
id: t.id ?? null,
title: t.title ?? null,
additionalInformation: t.additionalInformation ?? null,
tierSummary: t.tierSummary ?? null,
}))
: [],
}
: null,
}
}
export function mapApplication(raw: unknown): AshbyApplication {
const a = (raw ?? {}) as Unknown
const candidate = a.candidate as Unknown | undefined
const job = a.job as Unknown | undefined
const stage = a.currentInterviewStage as Unknown | undefined
const archiveReason = a.archiveReason as Unknown | undefined
return {
id: (a.id as string) ?? '',
createdAt: (a.createdAt as string) ?? null,
updatedAt: (a.updatedAt as string) ?? null,
status: (a.status as string) ?? '',
customFields: mapCustomFields(a.customFields),
candidate: {
id: (candidate?.id as string) ?? '',
name: (candidate?.name as string) ?? null,
primaryEmailAddress: mapContact(candidate?.primaryEmailAddress),
primaryPhoneNumber: mapContact(candidate?.primaryPhoneNumber),
},
currentInterviewStage: stage
? {
id: (stage.id as string) ?? '',
title: (stage.title as string) ?? null,
type: (stage.type as string) ?? null,
orderInInterviewPlan: (stage.orderInInterviewPlan as number) ?? null,
interviewStageGroupId: (stage.interviewStageGroupId as string) ?? null,
interviewPlanId: (stage.interviewPlanId as string) ?? null,
}
: null,
source: mapSource(a.source),
archiveReason: archiveReason
? {
id: (archiveReason.id as string) ?? '',
text: (archiveReason.text as string) ?? null,
reasonType: (archiveReason.reasonType as string) ?? null,
isArchived: (archiveReason.isArchived as boolean) ?? false,
customFields: mapCustomFields(archiveReason.customFields),
}
: null,
archivedAt: (a.archivedAt as string) ?? null,
job: {
id: (job?.id as string) ?? '',
title: (job?.title as string) ?? null,
locationId: (job?.locationId as string) ?? null,
departmentId: (job?.departmentId as string) ?? null,
},
creditedToUser: mapUserSummary(a.creditedToUser),
hiringTeam: mapHiringTeam(a.hiringTeam),
appliedViaJobPostingId: (a.appliedViaJobPostingId as string) ?? null,
submitterClientIp: (a.submitterClientIp as string) ?? null,
submitterUserAgent: (a.submitterUserAgent as string) ?? null,
applicationHistory: Array.isArray(a.applicationHistory)
? (a.applicationHistory as Unknown[]).map((h) => ({
id: (h.id as string) ?? '',
stageId: (h.stageId as string) ?? null,
stageNumber: (h.stageNumber as number) ?? null,
title: (h.title as string) ?? null,
enteredStageAt: (h.enteredStageAt as string) ?? null,
actorId: (h.actorId as string) ?? null,
}))
: [],
}
}
export const CONTACT_INFO_OUTPUT = {
type: 'object',
description: 'Contact info',
optional: true,
properties: {
value: { type: 'string', description: 'Value (email or phone number)' },
type: { type: 'string', description: 'Contact type (Personal, Work, Other)' },
isPrimary: { type: 'boolean', description: 'Whether this is the primary contact' },
},
} as const satisfies OutputProperty
export const CUSTOM_FIELDS_OUTPUT = {
type: 'array',
description: 'Custom field values',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Custom field UUID' },
title: { type: 'string', description: 'Field title' },
isPrivate: { type: 'boolean', description: 'Whether the field is private' },
valueLabel: { type: 'string', description: 'Human-readable value label', optional: true },
value: { type: 'string', description: 'Raw field value (type depends on fieldType)' },
},
},
} as const satisfies OutputProperty
export const FILE_HANDLE_OUTPUT = {
type: 'object',
description: 'File reference',
optional: true,
properties: {
id: { type: 'string', description: 'File UUID' },
name: { type: 'string', description: 'File name' },
handle: { type: 'string', description: 'File handle used with file.info' },
},
} as const satisfies OutputProperty
export const FILE_HANDLES_OUTPUT = {
type: 'array',
description: 'File references',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'File UUID' },
name: { type: 'string', description: 'File name' },
handle: { type: 'string', description: 'File handle used with file.info' },
},
},
} as const satisfies OutputProperty
export const USER_SUMMARY_OUTPUT = {
type: 'object',
description: 'User summary',
optional: true,
properties: {
id: { type: 'string', description: 'User UUID' },
firstName: { type: 'string', description: 'First name', optional: true },
lastName: { type: 'string', description: 'Last name', optional: true },
email: { type: 'string', description: 'Email', optional: true },
globalRole: { type: 'string', description: 'Role', optional: true },
isEnabled: { type: 'boolean', description: 'Whether enabled' },
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
managerId: { type: 'string', description: "User ID of the user's manager", optional: true },
},
} as const satisfies OutputProperty
export const SOURCE_SUMMARY_OUTPUT = {
type: 'object',
description: 'Attribution source',
optional: true,
properties: {
id: { type: 'string', description: 'Source UUID' },
title: { type: 'string', description: 'Source title' },
isArchived: { type: 'boolean', description: 'Whether archived' },
sourceType: {
type: 'object',
description: 'Source type grouping',
optional: true,
properties: {
id: { type: 'string', description: 'Source type UUID' },
title: { type: 'string', description: 'Source type title' },
isArchived: { type: 'boolean', description: 'Whether archived' },
},
},
},
} as const satisfies OutputProperty
export const HIRING_TEAM_OUTPUT = {
type: 'array',
description: 'Hiring team members',
items: {
type: 'object',
properties: {
userId: { type: 'string', description: 'User UUID' },
firstName: { type: 'string', description: 'First name' },
lastName: { type: 'string', description: 'Last name' },
email: { type: 'string', description: 'Email' },
role: { type: 'string', description: 'Hiring team role' },
},
},
} as const satisfies OutputProperty
export const CANDIDATE_OUTPUTS = {
id: { type: 'string', description: 'Candidate UUID' },
name: { type: 'string', description: 'Full name' },
primaryEmailAddress: { ...CONTACT_INFO_OUTPUT, description: 'Primary email contact info' },
primaryPhoneNumber: { ...CONTACT_INFO_OUTPUT, description: 'Primary phone contact info' },
emailAddresses: {
type: 'array',
description: 'All email addresses',
items: {
type: 'object',
properties: {
value: { type: 'string', description: 'Email address' },
type: { type: 'string', description: 'Contact type' },
isPrimary: { type: 'boolean', description: 'Whether primary' },
},
},
},
phoneNumbers: {
type: 'array',
description: 'All phone numbers',
items: {
type: 'object',
properties: {
value: { type: 'string', description: 'Phone number' },
type: { type: 'string', description: 'Contact type' },
isPrimary: { type: 'boolean', description: 'Whether primary' },
},
},
},
socialLinks: {
type: 'array',
description: 'Social network links',
items: {
type: 'object',
properties: {
type: { type: 'string', description: 'Link type (LinkedIn, GitHub, Twitter, etc.)' },
url: { type: 'string', description: 'Profile URL' },
},
},
},
linkedInUrl: { type: 'string', description: 'LinkedIn profile URL', optional: true },
githubUrl: { type: 'string', description: 'GitHub profile URL', optional: true },
profileUrl: { type: 'string', description: 'URL to the candidate Ashby profile', optional: true },
position: { type: 'string', description: 'Current position or title', optional: true },
company: { type: 'string', description: 'Current company', optional: true },
school: { type: 'string', description: 'Most recent school', optional: true },
timezone: { type: 'string', description: 'Candidate timezone', optional: true },
location: {
type: 'object',
description: 'Candidate location',
optional: true,
properties: {
id: { type: 'string', description: 'Location UUID', optional: true },
locationSummary: { type: 'string', description: 'Human-readable location summary' },
locationComponents: {
type: 'array',
description: 'Structured location parts (city, region, country, etc.)',
items: {
type: 'object',
properties: {
type: { type: 'string', description: 'Component type' },
name: { type: 'string', description: 'Component value' },
},
},
},
},
},
tags: {
type: 'array',
description: 'Tags applied to the candidate',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tag UUID' },
title: { type: 'string', description: 'Tag title' },
isArchived: { type: 'boolean', description: 'Whether archived' },
},
},
},
applicationIds: {
type: 'array',
description: 'IDs of associated applications',
items: { type: 'string', description: 'Application UUID' },
},
customFields: CUSTOM_FIELDS_OUTPUT,
resumeFileHandle: { ...FILE_HANDLE_OUTPUT, description: 'Resume file reference' },
fileHandles: { ...FILE_HANDLES_OUTPUT, description: 'All uploaded file references' },
source: SOURCE_SUMMARY_OUTPUT,
creditedToUser: { ...USER_SUMMARY_OUTPUT, description: 'User credited with sourcing' },
fraudStatus: { type: 'string', description: 'Fraud detection status', optional: true },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp' },
} as const satisfies Record<string, OutputProperty>
export const APPLICATION_OUTPUTS = {
id: { type: 'string', description: 'Application UUID' },
status: { type: 'string', description: 'Status (Active, Hired, Archived, Lead)' },
customFields: CUSTOM_FIELDS_OUTPUT,
candidate: {
type: 'object',
description: 'Associated candidate summary',
properties: {
id: { type: 'string', description: 'Candidate UUID' },
name: { type: 'string', description: 'Candidate name' },
primaryEmailAddress: { ...CONTACT_INFO_OUTPUT, description: 'Primary email' },
primaryPhoneNumber: { ...CONTACT_INFO_OUTPUT, description: 'Primary phone' },
},
},
currentInterviewStage: {
type: 'object',
description: 'Current interview stage',
optional: true,
properties: {
id: { type: 'string', description: 'Stage UUID' },
title: { type: 'string', description: 'Stage title' },
type: { type: 'string', description: 'Stage type' },
orderInInterviewPlan: {
type: 'number',
description: 'Position in plan',
optional: true,
},
interviewStageGroupId: { type: 'string', description: 'Stage group UUID', optional: true },
interviewPlanId: { type: 'string', description: 'Interview plan UUID', optional: true },
},
},
source: SOURCE_SUMMARY_OUTPUT,
archiveReason: {
type: 'object',
description: 'Reason for archival (when archived)',
optional: true,
properties: {
id: { type: 'string', description: 'Reason UUID' },
text: { type: 'string', description: 'Reason text' },
reasonType: { type: 'string', description: 'Reason category' },
isArchived: { type: 'boolean', description: 'Whether the reason is archived' },
customFields: CUSTOM_FIELDS_OUTPUT,
},
},
archivedAt: { type: 'string', description: 'ISO 8601 archive timestamp', optional: true },
job: {
type: 'object',
description: 'Associated job summary',
properties: {
id: { type: 'string', description: 'Job UUID' },
title: { type: 'string', description: 'Job title' },
locationId: { type: 'string', description: 'Location UUID', optional: true },
departmentId: { type: 'string', description: 'Department UUID', optional: true },
},
},
creditedToUser: { ...USER_SUMMARY_OUTPUT, description: 'User credited with the application' },
hiringTeam: HIRING_TEAM_OUTPUT,
appliedViaJobPostingId: {
type: 'string',
description: 'Job posting UUID the candidate applied through',
optional: true,
},
submitterClientIp: { type: 'string', description: 'Submitter IP address', optional: true },
submitterUserAgent: {
type: 'string',
description: 'Submitter browser user agent',
optional: true,
},
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp' },
applicationHistory: {
type: 'array',
description: 'Stage history (only populated by application.info, empty for list endpoints)',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'History entry UUID' },
stageId: { type: 'string', description: 'Interview stage UUID', optional: true },
stageNumber: { type: 'number', description: 'Stage order number', optional: true },
title: { type: 'string', description: 'Stage title at the time', optional: true },
enteredStageAt: {
type: 'string',
description: 'ISO 8601 timestamp the stage was entered',
optional: true,
},
actorId: {
type: 'string',
description: 'User UUID who triggered the stage change',
optional: true,
},
},
},
},
} as const satisfies Record<string, OutputProperty>
export const OPENINGS_OUTPUT = {
type: 'array',
description: 'Headcount openings associated with the job',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Opening UUID' },
openedAt: { type: 'string', description: 'Opening open timestamp', optional: true },
closedAt: { type: 'string', description: 'Opening close timestamp', optional: true },
isArchived: { type: 'boolean', description: 'Whether archived' },
archivedAt: { type: 'string', description: 'Archive timestamp', optional: true },
closeReasonId: { type: 'string', description: 'Close reason UUID', optional: true },
openingState: {
type: 'string',
description: 'Opening state (Approved, Open, Filled, Closed, Draft)',
optional: true,
},
latestVersion: {
type: 'object',
description: 'Latest opening version',
optional: true,
properties: {
id: { type: 'string', description: 'Version UUID', optional: true },
identifier: { type: 'string', description: 'Human-readable identifier' },
description: { type: 'string', description: 'Opening description' },
authorId: { type: 'string', description: 'Author user UUID', optional: true },
createdAt: { type: 'string', description: 'Version creation timestamp', optional: true },
teamId: { type: 'string', description: 'Team UUID', optional: true },
jobIds: {
type: 'array',
description: 'Associated job UUIDs',
items: { type: 'string', description: 'Job UUID' },
},
targetHireDate: { type: 'string', description: 'Target hire date', optional: true },
targetStartDate: { type: 'string', description: 'Target start date', optional: true },
isBackfill: { type: 'boolean', description: 'Whether this is a backfill opening' },
employmentType: { type: 'string', description: 'Employment type', optional: true },
locationIds: {
type: 'array',
description: 'Location UUIDs',
items: { type: 'string', description: 'Location UUID' },
},
hiringTeam: HIRING_TEAM_OUTPUT,
customFields: CUSTOM_FIELDS_OUTPUT,
},
},
},
},
} as const satisfies OutputProperty
function mapOfferVersion(raw: unknown): AshbyOfferVersion | null {
if (!raw || typeof raw !== 'object') return null
const v = raw as Unknown
const salary = v.salary as Unknown | undefined
return {
id: (v.id as string) ?? null,
startDate: (v.startDate as string) ?? null,
salary: salary
? {
currencyCode: (salary.currencyCode as string) ?? null,
value: (salary.value as number) ?? null,
}
: null,
createdAt: (v.createdAt as string) ?? null,
openingId: (v.openingId as string) ?? null,
customFields: mapCustomFields(v.customFields),
fileHandles: mapFileHandles(v.fileHandles),
author: mapUserSummary(v.author),
approvalStatus: (v.approvalStatus as string) ?? null,
}
}
export function mapOffer(raw: unknown): AshbyOffer {
const r = (raw ?? {}) as Unknown
return {
id: (r.id as string) ?? '',
decidedAt: (r.decidedAt as string) ?? null,
applicationId: (r.applicationId as string) ?? null,
acceptanceStatus: (r.acceptanceStatus as string) ?? null,
offerStatus: (r.offerStatus as string) ?? null,
latestVersion: mapOfferVersion(r.latestVersion),
}
}
export const OFFER_OUTPUTS = {
id: { type: 'string', description: 'Offer UUID' },
decidedAt: {
type: 'string',
description: 'Timestamp the offer was decided',
optional: true,
},
applicationId: {
type: 'string',
description: 'Associated application UUID',
optional: true,
},
acceptanceStatus: {
type: 'string',
description: 'Acceptance status (Accepted, Declined, Pending, etc.)',
optional: true,
},
offerStatus: {
type: 'string',
description: 'Offer status (e.g. WaitingOnCandidateResponse, CandidateAccepted)',
optional: true,
},
latestVersion: {
type: 'object',
description: 'Most recent version of the offer with pricing and metadata',
optional: true,
properties: {
id: { type: 'string', description: 'Version UUID', optional: true },
startDate: { type: 'string', description: 'Offer start date', optional: true },
salary: {
type: 'object',
description: 'Salary details',
optional: true,
properties: {
currencyCode: {
type: 'string',
description: 'ISO 4217 currency code',
optional: true,
},
value: { type: 'number', description: 'Salary amount', optional: true },
},
},
createdAt: {
type: 'string',
description: 'Version creation timestamp',
optional: true,
},
openingId: {
type: 'string',
description: 'Associated opening UUID',
optional: true,
},
customFields: CUSTOM_FIELDS_OUTPUT,
fileHandles: {
...FILE_HANDLES_OUTPUT,
description:
'Offer letter file handles (unsigned .pdf, .docx, and signed .pdf when generated)',
},
author: { ...USER_SUMMARY_OUTPUT, description: 'User who authored the version' },
approvalStatus: {
type: 'string',
description: 'Approval workflow status',
optional: true,
},
},
},
} as const satisfies Record<string, OutputProperty>
export const JOB_OUTPUTS = {
id: { type: 'string', description: 'Job UUID' },
title: { type: 'string', description: 'Job title' },
confidential: { type: 'boolean', description: 'Whether the job is confidential' },
status: { type: 'string', description: 'Status (Open, Closed, Draft, Archived)', optional: true },
employmentType: {
type: 'string',
description: 'Employment type (FullTime, PartTime, Intern, Contract, Temporary)',
optional: true,
},
locationId: { type: 'string', description: 'Primary location UUID', optional: true },
departmentId: { type: 'string', description: 'Department UUID', optional: true },
defaultInterviewPlanId: {
type: 'string',
description: 'Default interview plan UUID',
optional: true,
},
interviewPlanIds: {
type: 'array',
description: 'All interview plan UUIDs',
items: { type: 'string', description: 'Interview plan UUID' },
},
customFields: CUSTOM_FIELDS_OUTPUT,
jobPostingIds: {
type: 'array',
description: 'Associated job posting UUIDs',
items: { type: 'string', description: 'Job posting UUID' },
},
customRequisitionId: {
type: 'string',
description: 'Custom requisition identifier',
optional: true,
},
brandId: { type: 'string', description: 'Brand UUID', optional: true },
hiringTeam: HIRING_TEAM_OUTPUT,
author: { ...USER_SUMMARY_OUTPUT, description: 'Job author (creator)' },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true },
updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp', optional: true },
openedAt: { type: 'string', description: 'ISO 8601 opened timestamp', optional: true },
closedAt: { type: 'string', description: 'ISO 8601 closed timestamp', optional: true },
location: {
type: 'object',
description: 'Primary location details',
optional: true,
properties: {
id: { type: 'string', description: 'Location UUID', optional: true },
name: { type: 'string', description: 'Location name', optional: true },
externalName: { type: 'string', description: 'External display name', optional: true },
isArchived: { type: 'boolean', description: 'Whether archived' },
isRemote: { type: 'boolean', description: 'Whether remote' },
workplaceType: {
type: 'string',
description: 'Workplace type (OnSite, Remote, Hybrid)',
optional: true,
},
parentLocationId: {
type: 'string',
description: 'Parent location UUID',
optional: true,
},
type: { type: 'string', description: 'Location type', optional: true },
address: {
type: 'object',
description: 'Postal address',
optional: true,
properties: {
addressCountry: { type: 'string', description: 'Country', optional: true },
addressRegion: { type: 'string', description: 'State or region', optional: true },
addressLocality: { type: 'string', description: 'City or locality', optional: true },
postalCode: { type: 'string', description: 'Postal code', optional: true },
streetAddress: { type: 'string', description: 'Street address', optional: true },
},
},
},
},
openings: OPENINGS_OUTPUT,
compensation: {
type: 'object',
description:
'Compensation tiers for the job. Only present when the request includes the `compensation` expand parameter.',
optional: true,
properties: {
compensationTiers: {
type: 'array',
description: 'List of compensation tiers',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tier ID', optional: true },
title: { type: 'string', description: 'Tier title', optional: true },
additionalInformation: {
type: 'string',
description: 'Additional information about the tier',
optional: true,
},
tierSummary: {
type: 'string',
description: 'Human-readable summary of the tier',
optional: true,
},
},
},
},
},
},
} as const satisfies Record<string, OutputProperty>