chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import type { AshbyCandidate } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyAddCandidateTagParams {
apiKey: string
candidateId: string
tagId: string
}
interface AshbyAddCandidateTagResponse extends ToolResponse {
output: AshbyCandidate
}
export const addCandidateTagTool: ToolConfig<
AshbyAddCandidateTagParams,
AshbyAddCandidateTagResponse
> = {
id: 'ashby_add_candidate_tag',
name: 'Ashby Add Candidate Tag',
description: 'Adds a tag to a candidate in Ashby and returns the updated candidate.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
candidateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the candidate to add the tag to',
},
tagId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the tag to add',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.addTag',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
candidateId: params.candidateId.trim(),
tagId: params.tagId.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to add tag to candidate'))
}
return {
success: true,
output: mapCandidate(data.results),
}
},
outputs: CANDIDATE_OUTPUTS,
}
@@ -0,0 +1,87 @@
import type { AshbyApplication } from '@/tools/ashby/types'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyChangeApplicationStageParams {
apiKey: string
applicationId: string
interviewStageId: string
archiveReasonId?: string
}
interface AshbyChangeApplicationStageResponse extends ToolResponse {
output: AshbyApplication
}
export const changeApplicationStageTool: ToolConfig<
AshbyChangeApplicationStageParams,
AshbyChangeApplicationStageResponse
> = {
id: 'ashby_change_application_stage',
name: 'Ashby Change Application Stage',
description:
'Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
applicationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the application to update the stage of',
},
interviewStageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the interview stage to move the application to',
},
archiveReasonId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Archive reason UUID. Required when moving to an Archived stage, ignored otherwise',
},
},
request: {
url: 'https://api.ashbyhq.com/application.changeStage',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
applicationId: params.applicationId.trim(),
interviewStageId: params.interviewStageId.trim(),
}
if (params.archiveReasonId) body.archiveReasonId = params.archiveReasonId.trim()
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to change application stage'))
}
return {
success: true,
output: mapApplication(data.results),
}
},
outputs: APPLICATION_OUTPUTS,
}
+119
View File
@@ -0,0 +1,119 @@
import type { AshbyApplication } from '@/tools/ashby/types'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyCreateApplicationParams {
apiKey: string
candidateId: string
jobId: string
interviewPlanId?: string
interviewStageId?: string
sourceId?: string
creditedToUserId?: string
createdAt?: string
}
interface AshbyCreateApplicationResponse extends ToolResponse {
output: AshbyApplication
}
export const createApplicationTool: ToolConfig<
AshbyCreateApplicationParams,
AshbyCreateApplicationResponse
> = {
id: 'ashby_create_application',
name: 'Ashby Create Application',
description:
'Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
candidateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the candidate to consider for the job',
},
jobId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the job to consider the candidate for',
},
interviewPlanId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the interview plan to use (defaults to the job default plan)',
},
interviewStageId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'UUID of the interview stage to place the application in (defaults to first Lead stage)',
},
sourceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the source to set on the application',
},
creditedToUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the user the application is credited to',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ISO 8601 timestamp to set as the application creation date (defaults to now)',
},
},
request: {
url: 'https://api.ashbyhq.com/application.create',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
jobId: params.jobId.trim(),
}
if (params.interviewPlanId) body.interviewPlanId = params.interviewPlanId.trim()
if (params.interviewStageId) body.interviewStageId = params.interviewStageId.trim()
if (params.sourceId) body.sourceId = params.sourceId.trim()
if (params.creditedToUserId) body.creditedToUserId = params.creditedToUserId.trim()
if (params.createdAt) body.createdAt = params.createdAt
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to create application'))
}
return {
success: true,
output: mapApplication(data.results),
}
},
outputs: APPLICATION_OUTPUTS,
}
+129
View File
@@ -0,0 +1,129 @@
import type { AshbyCreateCandidateParams, AshbyCreateCandidateResponse } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const createCandidateTool: ToolConfig<
AshbyCreateCandidateParams,
AshbyCreateCandidateResponse
> = {
id: 'ashby_create_candidate',
name: 'Ashby Create Candidate',
description: 'Creates a new candidate record in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The candidate full name',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Primary email address for the candidate',
},
phoneNumber: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Primary phone number for the candidate',
},
linkedInUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'LinkedIn profile URL',
},
githubUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'GitHub profile URL',
},
website: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Personal website URL',
},
sourceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the source to attribute the candidate to',
},
creditedToUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the Ashby user to credit with sourcing this candidate',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now.',
},
alternateEmailAddresses: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Array of additional email address strings to add to the candidate, e.g. ["a@x.com","b@y.com"]',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.create',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
name: params.name,
}
if (params.email) body.email = params.email
if (params.phoneNumber) body.phoneNumber = params.phoneNumber
if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl
if (params.githubUrl) body.githubUrl = params.githubUrl
if (params.website) body.website = params.website
if (params.sourceId) body.sourceId = params.sourceId.trim()
if (params.creditedToUserId) body.creditedToUserId = params.creditedToUserId.trim()
if (params.createdAt) body.createdAt = params.createdAt
if (
Array.isArray(params.alternateEmailAddresses) &&
params.alternateEmailAddresses.length > 0
)
body.alternateEmailAddresses = params.alternateEmailAddresses
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to create candidate'))
}
return {
success: true,
output: mapCandidate(data.results),
}
},
outputs: CANDIDATE_OUTPUTS,
}
+128
View File
@@ -0,0 +1,128 @@
import type { AshbyCreateNoteParams, AshbyCreateNoteResponse } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const createNoteTool: ToolConfig<AshbyCreateNoteParams, AshbyCreateNoteResponse> = {
id: 'ashby_create_note',
name: 'Ashby Create Note',
description:
'Creates a note on a candidate in Ashby. Supports plain text and HTML content (bold, italic, underline, links, lists, code).',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
candidateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the candidate to add the note to',
},
note: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The note content. If noteType is text/html, supports: <b>, <i>, <u>, <a>, <ul>, <ol>, <li>, <code>, <pre>',
},
noteType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Content type of the note: text/plain (default) or text/html',
},
sendNotifications: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to send notifications to subscribed users (default false)',
},
isPrivate: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the note is private (only visible to the author)',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now.',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.createNote',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
sendNotifications: params.sendNotifications ?? false,
}
if (params.noteType === 'text/html') {
body.note = {
type: 'text/html',
value: params.note,
}
} else {
body.note = params.note
}
if (params.isPrivate !== undefined) body.isPrivate = params.isPrivate
if (params.createdAt) body.createdAt = params.createdAt
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to create note'))
}
const r = data.results
const author = r.author
return {
success: true,
output: {
id: r.id ?? '',
createdAt: r.createdAt ?? null,
isPrivate: r.isPrivate ?? false,
content: r.content ?? null,
author: author
? {
id: author.id ?? '',
firstName: author.firstName ?? null,
lastName: author.lastName ?? null,
email: author.email ?? null,
}
: null,
},
}
},
outputs: {
id: { type: 'string', description: 'Created note UUID' },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true },
isPrivate: { type: 'boolean', description: 'Whether the note is private' },
content: { type: 'string', description: 'Note content', optional: true },
author: {
type: 'object',
description: 'Author of the note',
optional: true,
properties: {
id: { type: 'string', description: 'Author user UUID' },
firstName: { type: 'string', description: 'Author first name', optional: true },
lastName: { type: 'string', description: 'Author last name', optional: true },
email: { type: 'string', description: 'Author email', optional: true },
},
},
},
}
+66
View File
@@ -0,0 +1,66 @@
import type { AshbyApplication } from '@/tools/ashby/types'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyGetApplicationParams {
apiKey: string
applicationId: string
}
interface AshbyGetApplicationResponse extends ToolResponse {
output: AshbyApplication
}
export const getApplicationTool: ToolConfig<
AshbyGetApplicationParams,
AshbyGetApplicationResponse
> = {
id: 'ashby_get_application',
name: 'Ashby Get Application',
description: 'Retrieves full details about a single application by its ID.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
applicationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the application to fetch',
},
},
request: {
url: 'https://api.ashbyhq.com/application.info',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
applicationId: params.applicationId.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to get application'))
}
return {
success: true,
output: mapApplication(data.results),
}
},
outputs: APPLICATION_OUTPUTS,
}
+54
View File
@@ -0,0 +1,54 @@
import type { AshbyGetCandidateParams, AshbyGetCandidateResponse } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const getCandidateTool: ToolConfig<AshbyGetCandidateParams, AshbyGetCandidateResponse> = {
id: 'ashby_get_candidate',
name: 'Ashby Get Candidate',
description: 'Retrieves full details about a single candidate by their ID.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
candidateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the candidate to fetch',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.info',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
id: params.candidateId.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to get candidate'))
}
return {
success: true,
output: mapCandidate(data.results),
}
},
outputs: CANDIDATE_OUTPUTS,
}
+50
View File
@@ -0,0 +1,50 @@
import type { AshbyGetJobParams, AshbyGetJobResponse } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage, JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const getJobTool: ToolConfig<AshbyGetJobParams, AshbyGetJobResponse> = {
id: 'ashby_get_job',
name: 'Ashby Get Job',
description: 'Retrieves full details about a single job by its ID.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
jobId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the job to fetch',
},
},
request: {
url: 'https://api.ashbyhq.com/job.info',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
id: params.jobId.trim(),
expand: ['openings', 'location', 'compensation'],
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to get job'))
}
return {
success: true,
output: mapJob(data.results),
}
},
outputs: JOB_OUTPUTS,
}
+422
View File
@@ -0,0 +1,422 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyGetJobPostingParams {
apiKey: string
jobPostingId: string
jobBoardId?: string
expandJob?: boolean
}
interface AshbyDescriptionPart {
html: string | null
plain: string | null
}
interface AshbyJobPosting {
id: string
title: string
descriptionPlain: string | null
descriptionHtml: string | null
descriptionSocial: string | null
descriptionParts: {
descriptionOpening: AshbyDescriptionPart | null
descriptionBody: AshbyDescriptionPart | null
descriptionClosing: AshbyDescriptionPart | null
} | null
departmentName: string | null
teamName: string | null
teamNameHierarchy: string[]
jobId: string | null
locationName: string | null
locationIds: {
primaryLocationId: string | null
secondaryLocationIds: string[]
} | null
address: {
postalAddress: {
addressCountry: string | null
addressRegion: string | null
addressLocality: string | null
postalCode: string | null
streetAddress: string | null
} | null
} | null
isRemote: boolean
workplaceType: string | null
employmentType: string | null
isListed: boolean
suppressDescriptionOpening: boolean
suppressDescriptionClosing: boolean
publishedDate: string | null
applicationDeadline: string | null
externalLink: string | null
applyLink: string | null
compensation: {
compensationTierSummary: string | null
summaryComponents: Array<{
summary: string | null
compensationTypeLabel: string | null
interval: string | null
currencyCode: string | null
minValue: number | null
maxValue: number | null
}>
shouldDisplayCompensationOnJobBoard: boolean
} | null
applicationLimitCalloutHtml: string | null
updatedAt: string | null
job: Record<string, unknown> | null
}
interface AshbyGetJobPostingResponse extends ToolResponse {
output: AshbyJobPosting
}
function mapDescriptionPart(raw: unknown): AshbyDescriptionPart | null {
if (!raw || typeof raw !== 'object') return null
const p = raw as Record<string, unknown>
return {
html: (p.html as string) ?? null,
plain: (p.plain as string) ?? null,
}
}
export const getJobPostingTool: ToolConfig<AshbyGetJobPostingParams, AshbyGetJobPostingResponse> = {
id: 'ashby_get_job_posting',
name: 'Ashby Get Job Posting',
description: 'Retrieves full details about a single job posting by its ID.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
jobPostingId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the job posting to fetch',
},
jobBoardId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Optional job board UUID. If omitted, returns posting for the external job board.',
},
expandJob: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to expand and include the related job object in the response',
},
},
request: {
url: 'https://api.ashbyhq.com/jobPosting.info',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
jobPostingId: params.jobPostingId.trim(),
}
if (params.jobBoardId) body.jobBoardId = params.jobBoardId.trim()
if (params.expandJob) body.expand = ['job']
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to get job posting'))
}
const r = (data.results ?? {}) as Record<string, unknown> & {
descriptionParts?: Record<string, unknown>
locationIds?: { primaryLocationId?: string; secondaryLocationIds?: string[] }
address?: { postalAddress?: Record<string, unknown> }
compensation?: Record<string, unknown> & {
summaryComponents?: Array<Record<string, unknown>>
}
}
const pa = r.address?.postalAddress
const comp = r.compensation
const summaryComponents = Array.isArray(comp?.summaryComponents) ? comp.summaryComponents : []
const descParts = r.descriptionParts
return {
success: true,
output: {
id: (r.id as string) ?? '',
title: (r.title as string) ?? '',
descriptionPlain: (r.descriptionPlain as string) ?? null,
descriptionHtml: (r.descriptionHtml as string) ?? null,
descriptionSocial: (r.descriptionSocial as string) ?? null,
descriptionParts: descParts
? {
descriptionOpening: mapDescriptionPart(descParts.descriptionOpening),
descriptionBody: mapDescriptionPart(descParts.descriptionBody),
descriptionClosing: mapDescriptionPart(descParts.descriptionClosing),
}
: null,
departmentName: (r.departmentName as string) ?? null,
teamName: (r.teamName as string) ?? null,
teamNameHierarchy: Array.isArray(r.teamNameHierarchy)
? (r.teamNameHierarchy as string[])
: [],
jobId: (r.jobId as string) ?? null,
locationName: (r.locationName as string) ?? null,
locationIds: r.locationIds
? {
primaryLocationId: r.locationIds.primaryLocationId ?? null,
secondaryLocationIds: Array.isArray(r.locationIds.secondaryLocationIds)
? r.locationIds.secondaryLocationIds
: [],
}
: null,
address: r.address
? {
postalAddress: pa
? {
addressCountry: (pa.addressCountry as string) ?? null,
addressRegion: (pa.addressRegion as string) ?? null,
addressLocality: (pa.addressLocality as string) ?? null,
postalCode: (pa.postalCode as string) ?? null,
streetAddress: (pa.streetAddress as string) ?? null,
}
: null,
}
: null,
isRemote: (r.isRemote as boolean) ?? false,
workplaceType: (r.workplaceType as string) ?? null,
employmentType: (r.employmentType as string) ?? null,
isListed: (r.isListed as boolean) ?? false,
suppressDescriptionOpening: (r.suppressDescriptionOpening as boolean) ?? false,
suppressDescriptionClosing: (r.suppressDescriptionClosing as boolean) ?? false,
publishedDate: (r.publishedDate as string) ?? null,
applicationDeadline: (r.applicationDeadline as string) ?? null,
externalLink: (r.externalLink as string) ?? null,
applyLink: (r.applyLink as string) ?? null,
compensation: comp
? {
compensationTierSummary: (comp.compensationTierSummary as string) ?? null,
summaryComponents: summaryComponents.map((c) => ({
summary: (c.summary as string) ?? null,
compensationTypeLabel: (c.compensationTypeLabel as string) ?? null,
interval: (c.interval as string) ?? null,
currencyCode: (c.currencyCode as string) ?? null,
minValue: (c.minValue as number) ?? null,
maxValue: (c.maxValue as number) ?? null,
})),
shouldDisplayCompensationOnJobBoard:
(comp.shouldDisplayCompensationOnJobBoard as boolean) ?? false,
}
: null,
applicationLimitCalloutHtml: (r.applicationLimitCalloutHtml as string) ?? null,
updatedAt: (r.updatedAt as string) ?? null,
job: (r.job as Record<string, unknown>) ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Job posting UUID' },
title: { type: 'string', description: 'Job posting title' },
descriptionPlain: {
type: 'string',
description: 'Full description in plain text',
optional: true,
},
descriptionHtml: {
type: 'string',
description: 'Full description in HTML',
optional: true,
},
descriptionSocial: {
type: 'string',
description: 'Shortened description for social sharing (max 200 chars)',
optional: true,
},
descriptionParts: {
type: 'object',
description: 'Description broken into opening, body, and closing sections',
optional: true,
properties: {
descriptionOpening: {
type: 'object',
description: 'Opening (from Job Boards theme settings)',
optional: true,
properties: {
html: { type: 'string', description: 'HTML content', optional: true },
plain: { type: 'string', description: 'Plain text content', optional: true },
},
},
descriptionBody: {
type: 'object',
description: 'Main description body',
optional: true,
properties: {
html: { type: 'string', description: 'HTML content', optional: true },
plain: { type: 'string', description: 'Plain text content', optional: true },
},
},
descriptionClosing: {
type: 'object',
description: 'Closing (from Job Boards theme settings)',
optional: true,
properties: {
html: { type: 'string', description: 'HTML content', optional: true },
plain: { type: 'string', description: 'Plain text content', optional: true },
},
},
},
},
departmentName: { type: 'string', description: 'Department name', optional: true },
teamName: { type: 'string', description: 'Team name', optional: true },
teamNameHierarchy: {
type: 'array',
description: 'Hierarchy of team names from root to team',
items: { type: 'string', description: 'Team name' },
},
jobId: { type: 'string', description: 'Associated job UUID', optional: true },
locationName: { type: 'string', description: 'Primary location name', optional: true },
locationIds: {
type: 'object',
description: 'Primary and secondary location UUIDs',
optional: true,
properties: {
primaryLocationId: {
type: 'string',
description: 'Primary location UUID',
optional: true,
},
secondaryLocationIds: {
type: 'array',
description: 'Secondary location UUIDs',
items: { type: 'string', description: 'Location UUID' },
},
},
},
address: {
type: 'object',
description: 'Postal address of the posting location',
optional: true,
properties: {
postalAddress: {
type: 'object',
description: 'Structured 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 },
},
},
},
},
isRemote: { type: 'boolean', description: 'Whether the posting is remote' },
workplaceType: {
type: 'string',
description: 'Workplace type (OnSite, Remote, Hybrid)',
optional: true,
},
employmentType: {
type: 'string',
description: 'Employment type (FullTime, PartTime, Intern, Contract, Temporary)',
optional: true,
},
isListed: { type: 'boolean', description: 'Whether publicly listed on the job board' },
suppressDescriptionOpening: {
type: 'boolean',
description: 'Whether the theme opening is hidden on this posting',
},
suppressDescriptionClosing: {
type: 'boolean',
description: 'Whether the theme closing is hidden on this posting',
},
publishedDate: { type: 'string', description: 'ISO 8601 published date', optional: true },
applicationDeadline: {
type: 'string',
description: 'ISO 8601 application deadline',
optional: true,
},
externalLink: {
type: 'string',
description: 'External link to the job posting',
optional: true,
},
applyLink: {
type: 'string',
description: 'Direct apply link',
optional: true,
},
compensation: {
type: 'object',
description: 'Compensation details for the posting',
optional: true,
properties: {
compensationTierSummary: {
type: 'string',
description: 'Human-readable tier summary',
optional: true,
},
summaryComponents: {
type: 'array',
description: 'Structured compensation components',
items: {
type: 'object',
properties: {
summary: { type: 'string', description: 'Component summary', optional: true },
compensationTypeLabel: {
type: 'string',
description: 'Component type label (Salary, Commission, Bonus, Equity, etc.)',
optional: true,
},
interval: {
type: 'string',
description: 'Payment interval (e.g. annual, hourly)',
optional: true,
},
currencyCode: {
type: 'string',
description: 'ISO 4217 currency code',
optional: true,
},
minValue: { type: 'number', description: 'Minimum value', optional: true },
maxValue: { type: 'number', description: 'Maximum value', optional: true },
},
},
},
shouldDisplayCompensationOnJobBoard: {
type: 'boolean',
description: 'Whether compensation is shown on the job board',
},
},
},
applicationLimitCalloutHtml: {
type: 'string',
description: 'HTML callout shown when the application limit is reached',
optional: true,
},
updatedAt: {
type: 'string',
description: 'ISO 8601 last update timestamp',
optional: true,
},
job: {
type: 'object',
description:
'The expanded job object, only present when the request was made with expandJob=true',
optional: true,
},
},
}
+58
View File
@@ -0,0 +1,58 @@
import type { AshbyOffer } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage, mapOffer, OFFER_OUTPUTS } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyGetOfferParams {
apiKey: string
offerId: string
}
interface AshbyGetOfferResponse extends ToolResponse {
output: AshbyOffer
}
export const getOfferTool: ToolConfig<AshbyGetOfferParams, AshbyGetOfferResponse> = {
id: 'ashby_get_offer',
name: 'Ashby Get Offer',
description: 'Retrieves full details about a single offer by its ID.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
offerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the offer to fetch',
},
},
request: {
url: 'https://api.ashbyhq.com/offer.info',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
offerId: params.offerId.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to get offer'))
}
return {
success: true,
output: mapOffer(data.results),
}
},
outputs: OFFER_OUTPUTS,
}
+59
View File
@@ -0,0 +1,59 @@
import { addCandidateTagTool } from '@/tools/ashby/add_candidate_tag'
import { changeApplicationStageTool } from '@/tools/ashby/change_application_stage'
import { createApplicationTool } from '@/tools/ashby/create_application'
import { createCandidateTool } from '@/tools/ashby/create_candidate'
import { createNoteTool } from '@/tools/ashby/create_note'
import { getApplicationTool } from '@/tools/ashby/get_application'
import { getCandidateTool } from '@/tools/ashby/get_candidate'
import { getJobTool } from '@/tools/ashby/get_job'
import { getJobPostingTool } from '@/tools/ashby/get_job_posting'
import { getOfferTool } from '@/tools/ashby/get_offer'
import { listApplicationsTool } from '@/tools/ashby/list_applications'
import { listArchiveReasonsTool } from '@/tools/ashby/list_archive_reasons'
import { listCandidateTagsTool } from '@/tools/ashby/list_candidate_tags'
import { listCandidatesTool } from '@/tools/ashby/list_candidates'
import { listCustomFieldsTool } from '@/tools/ashby/list_custom_fields'
import { listDepartmentsTool } from '@/tools/ashby/list_departments'
import { listInterviewsTool } from '@/tools/ashby/list_interviews'
import { listJobPostingsTool } from '@/tools/ashby/list_job_postings'
import { listJobsTool } from '@/tools/ashby/list_jobs'
import { listLocationsTool } from '@/tools/ashby/list_locations'
import { listNotesTool } from '@/tools/ashby/list_notes'
import { listOffersTool } from '@/tools/ashby/list_offers'
import { listOpeningsTool } from '@/tools/ashby/list_openings'
import { listSourcesTool } from '@/tools/ashby/list_sources'
import { listUsersTool } from '@/tools/ashby/list_users'
import { removeCandidateTagTool } from '@/tools/ashby/remove_candidate_tag'
import { searchCandidatesTool } from '@/tools/ashby/search_candidates'
import { updateCandidateTool } from '@/tools/ashby/update_candidate'
export const ashbyAddCandidateTagTool = addCandidateTagTool
export const ashbyChangeApplicationStageTool = changeApplicationStageTool
export const ashbyCreateApplicationTool = createApplicationTool
export const ashbyCreateCandidateTool = createCandidateTool
export const ashbyCreateNoteTool = createNoteTool
export const ashbyGetApplicationTool = getApplicationTool
export const ashbyGetCandidateTool = getCandidateTool
export const ashbyGetJobTool = getJobTool
export const ashbyGetJobPostingTool = getJobPostingTool
export const ashbyGetOfferTool = getOfferTool
export const ashbyListApplicationsTool = listApplicationsTool
export const ashbyListArchiveReasonsTool = listArchiveReasonsTool
export const ashbyListCandidateTagsTool = listCandidateTagsTool
export const ashbyListCandidatesTool = listCandidatesTool
export const ashbyListCustomFieldsTool = listCustomFieldsTool
export const ashbyListDepartmentsTool = listDepartmentsTool
export const ashbyListInterviewsTool = listInterviewsTool
export const ashbyListJobPostingsTool = listJobPostingsTool
export const ashbyListJobsTool = listJobsTool
export const ashbyListLocationsTool = listLocationsTool
export const ashbyListNotesTool = listNotesTool
export const ashbyListOffersTool = listOffersTool
export const ashbyListOpeningsTool = listOpeningsTool
export const ashbyListSourcesTool = listSourcesTool
export const ashbyListUsersTool = listUsersTool
export const ashbyRemoveCandidateTagTool = removeCandidateTagTool
export const ashbySearchCandidatesTool = searchCandidatesTool
export const ashbyUpdateCandidateTool = updateCandidateTool
export * from './types'
+117
View File
@@ -0,0 +1,117 @@
import type {
AshbyListApplicationsParams,
AshbyListApplicationsResponse,
} from '@/tools/ashby/types'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const listApplicationsTool: ToolConfig<
AshbyListApplicationsParams,
AshbyListApplicationsResponse
> = {
id: 'ashby_list_applications',
name: 'Ashby List Applications',
description:
'Lists all applications in an Ashby organization with pagination and optional filters for status, job, and creation date.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by application status: Active, Hired, Archived, or Lead',
},
jobId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter applications by a specific job UUID',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter to applications created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
},
request: {
url: 'https://api.ashbyhq.com/application.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.status) body.status = params.status
if (params.jobId) body.jobId = params.jobId.trim()
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list applications'))
}
return {
success: true,
output: {
applications: (data.results ?? []).map(mapApplication),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
applications: {
type: 'array',
description: 'List of applications',
items: {
type: 'object',
properties: APPLICATION_OUTPUTS,
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
@@ -0,0 +1,95 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListArchiveReasonsParams {
apiKey: string
includeArchived?: boolean
}
interface AshbyArchiveReason {
id: string
text: string
reasonType: string
isArchived: boolean
}
interface AshbyListArchiveReasonsResponse extends ToolResponse {
output: {
archiveReasons: AshbyArchiveReason[]
}
}
export const listArchiveReasonsTool: ToolConfig<
AshbyListArchiveReasonsParams,
AshbyListArchiveReasonsResponse
> = {
id: 'ashby_list_archive_reasons',
name: 'Ashby List Archive Reasons',
description: 'Lists all archive reasons configured in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to include archived archive reasons in the response (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/archiveReason.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list archive reasons'))
}
return {
success: true,
output: {
archiveReasons: (data.results ?? []).map((r: Record<string, unknown>) => ({
id: (r.id as string) ?? '',
text: (r.text as string) ?? '',
reasonType: (r.reasonType as string) ?? '',
isArchived: (r.isArchived as boolean) ?? false,
})),
},
}
},
outputs: {
archiveReasons: {
type: 'array',
description: 'List of archive reasons',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Archive reason UUID' },
text: { type: 'string', description: 'Archive reason text' },
reasonType: {
type: 'string',
description: 'Reason type (RejectedByCandidate, RejectedByOrg, Other)',
},
isArchived: { type: 'boolean', description: 'Whether the reason is archived' },
},
},
},
},
}
+133
View File
@@ -0,0 +1,133 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListCandidateTagsParams {
apiKey: string
includeArchived?: boolean
cursor?: string
syncToken?: string
perPage?: number
}
interface AshbyCandidateTag {
id: string
title: string
isArchived: boolean
}
interface AshbyListCandidateTagsResponse extends ToolResponse {
output: {
tags: AshbyCandidateTag[]
moreDataAvailable: boolean
nextCursor: string | null
syncToken: string | null
}
}
export const listCandidateTagsTool: ToolConfig<
AshbyListCandidateTagsParams,
AshbyListCandidateTagsResponse
> = {
id: 'ashby_list_candidate_tags',
name: 'Ashby List Candidate Tags',
description: 'Lists all candidate tags configured in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to include archived candidate tags (default false)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sync token from a previous response to fetch only changed results',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
},
request: {
url: 'https://api.ashbyhq.com/candidateTag.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
if (params.cursor) body.cursor = params.cursor
if (params.syncToken) body.syncToken = params.syncToken
if (params.perPage) body.limit = params.perPage
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list candidate tags'))
}
return {
success: true,
output: {
tags: (data.results ?? []).map((t: Record<string, unknown>) => ({
id: (t.id as string) ?? '',
title: (t.title as string) ?? '',
isArchived: (t.isArchived as boolean) ?? false,
})),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
syncToken: data.syncToken ?? null,
},
}
},
outputs: {
tags: {
type: 'array',
description: 'List of candidate tags',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tag UUID' },
title: { type: 'string', description: 'Tag title' },
isArchived: { type: 'boolean', description: 'Whether the tag is archived' },
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
syncToken: {
type: 'string',
description: 'Sync token to use for incremental updates in future requests',
optional: true,
},
},
}
+99
View File
@@ -0,0 +1,99 @@
import type { AshbyListCandidatesParams, AshbyListCandidatesResponse } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const listCandidatesTool: ToolConfig<
AshbyListCandidatesParams,
AshbyListCandidatesResponse
> = {
id: 'ashby_list_candidates',
name: 'Ashby List Candidates',
description: 'Lists all candidates in an Ashby organization with cursor-based pagination.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return candidates created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list candidates'))
}
return {
success: true,
output: {
candidates: (data.results ?? []).map(mapCandidate),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
candidates: {
type: 'array',
description: 'List of candidates',
items: {
type: 'object',
properties: CANDIDATE_OUTPUTS,
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
+183
View File
@@ -0,0 +1,183 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListCustomFieldsParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
includeArchived?: boolean
}
interface AshbyCustomFieldDefinition {
id: string
title: string
isPrivate: boolean
fieldType: string
objectType: string
isArchived: boolean
isRequired: boolean
selectableValues: Array<{
label: string
value: string
isArchived: boolean
}>
}
interface AshbyListCustomFieldsResponse extends ToolResponse {
output: {
customFields: AshbyCustomFieldDefinition[]
moreDataAvailable: boolean
nextCursor: string | null
syncToken: string | null
}
}
export const listCustomFieldsTool: ToolConfig<
AshbyListCustomFieldsParams,
AshbyListCustomFieldsResponse
> = {
id: 'ashby_list_custom_fields',
name: 'Ashby List Custom Fields',
description: 'Lists all custom field definitions configured in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default and max 100)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived custom fields in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/customField.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.syncToken) body.syncToken = params.syncToken
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list custom fields'))
}
return {
success: true,
output: {
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
syncToken: data.syncToken ?? null,
customFields: (data.results ?? []).map(
(f: Record<string, unknown> & { selectableValues?: Array<Record<string, unknown>> }) => ({
id: (f.id as string) ?? '',
title: (f.title as string) ?? '',
isPrivate: (f.isPrivate as boolean) ?? false,
fieldType: (f.fieldType as string) ?? '',
objectType: (f.objectType as string) ?? '',
isArchived: (f.isArchived as boolean) ?? false,
isRequired: (f.isRequired as boolean) ?? false,
selectableValues: Array.isArray(f.selectableValues)
? f.selectableValues.map((v) => ({
label: (v.label as string) ?? '',
value: (v.value as string) ?? '',
isArchived: (v.isArchived as boolean) ?? false,
}))
: [],
})
),
},
}
},
outputs: {
customFields: {
type: 'array',
description: 'List of custom field definitions',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Custom field UUID' },
title: { type: 'string', description: 'Custom field title' },
isPrivate: {
type: 'boolean',
description: 'Whether the custom field is private',
},
fieldType: {
type: 'string',
description:
'Field data type (MultiValueSelect, NumberRange, String, Date, ValueSelect, Number, Currency, Boolean, LongText, CompensationRange)',
},
objectType: {
type: 'string',
description:
'Object type the field applies to (Application, Candidate, Employee, Job, Offer, Opening, Talent_Project)',
},
isArchived: { type: 'boolean', description: 'Whether the custom field is archived' },
isRequired: { type: 'boolean', description: 'Whether a value is required' },
selectableValues: {
type: 'array',
description:
'Selectable values for MultiValueSelect fields (empty for other field types)',
items: {
type: 'object',
properties: {
label: { type: 'string', description: 'Display label' },
value: { type: 'string', description: 'Stored value' },
isArchived: { type: 'boolean', description: 'Whether archived' },
},
},
},
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
syncToken: {
type: 'string',
description: 'Opaque sync token returned after the last page; pass on next sync',
optional: true,
},
},
}
+168
View File
@@ -0,0 +1,168 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListDepartmentsParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
includeArchived?: boolean
}
interface AshbyDepartment {
id: string
name: string
externalName: string | null
isArchived: boolean
parentId: string | null
createdAt: string | null
updatedAt: string | null
extraData: Record<string, unknown> | null
}
interface AshbyListDepartmentsResponse extends ToolResponse {
output: {
departments: AshbyDepartment[]
moreDataAvailable: boolean
nextCursor: string | null
syncToken: string | null
}
}
export const listDepartmentsTool: ToolConfig<
AshbyListDepartmentsParams,
AshbyListDepartmentsResponse
> = {
id: 'ashby_list_departments',
name: 'Ashby List Departments',
description: 'Lists all departments in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default and max 100)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived departments in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/department.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.syncToken) body.syncToken = params.syncToken
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list departments'))
}
return {
success: true,
output: {
departments: (data.results ?? []).map((d: Record<string, unknown>) => ({
id: (d.id as string) ?? '',
name: (d.name as string) ?? '',
externalName: (d.externalName as string) ?? null,
isArchived: (d.isArchived as boolean) ?? false,
parentId: (d.parentId as string) ?? null,
createdAt: (d.createdAt as string) ?? null,
updatedAt: (d.updatedAt as string) ?? null,
extraData: (d.extraData as Record<string, unknown>) ?? null,
})),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
syncToken: data.syncToken ?? null,
},
}
},
outputs: {
departments: {
type: 'array',
description: 'List of departments',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Department UUID' },
name: { type: 'string', description: 'Department name' },
externalName: {
type: 'string',
description: 'Candidate-facing name used on job boards',
optional: true,
},
isArchived: { type: 'boolean', description: 'Whether the department is archived' },
parentId: {
type: 'string',
description: 'Parent department UUID',
optional: true,
},
createdAt: {
type: 'string',
description: 'ISO 8601 creation timestamp',
optional: true,
},
updatedAt: {
type: 'string',
description: 'ISO 8601 last update timestamp',
optional: true,
},
extraData: {
type: 'json',
description: 'Free-form key-value metadata',
optional: true,
},
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
syncToken: {
type: 'string',
description: 'Opaque sync token returned after the last page; pass on next sync',
optional: true,
},
},
}
+286
View File
@@ -0,0 +1,286 @@
import type { AshbyUserSummary } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
mapUserSummary,
USER_SUMMARY_OUTPUT,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListInterviewSchedulesParams {
apiKey: string
applicationId?: string
interviewStageId?: string
cursor?: string
perPage?: number
createdAfter?: string
}
interface AshbyInterviewEvent {
id: string
interviewId: string | null
interviewScheduleId: string | null
interviewerUserIds: string[]
createdAt: string | null
updatedAt: string | null
startTime: string | null
endTime: string | null
feedbackLink: string | null
location: string | null
meetingLink: string | null
hasSubmittedFeedback: boolean
}
interface AshbyInterviewSchedule {
id: string
status: string | null
applicationId: string
interviewStageId: string | null
scheduledBy: AshbyUserSummary | null
createdAt: string | null
updatedAt: string | null
interviewEvents: AshbyInterviewEvent[]
}
interface AshbyListInterviewSchedulesResponse extends ToolResponse {
output: {
interviewSchedules: AshbyInterviewSchedule[]
moreDataAvailable: boolean
nextCursor: string | null
}
}
type UnknownRecord = Record<string, unknown>
function mapInterviewEvent(raw: unknown): AshbyInterviewEvent | null {
if (!raw || typeof raw !== 'object') return null
const e = raw as UnknownRecord
return {
id: (e.id as string) ?? '',
interviewId: (e.interviewId as string) ?? null,
interviewScheduleId: (e.interviewScheduleId as string) ?? null,
interviewerUserIds: Array.isArray(e.interviewerUserIds)
? (e.interviewerUserIds as string[])
: [],
createdAt: (e.createdAt as string) ?? null,
updatedAt: (e.updatedAt as string) ?? null,
startTime: (e.startTime as string) ?? null,
endTime: (e.endTime as string) ?? null,
feedbackLink: (e.feedbackLink as string) ?? null,
location: (e.location as string) ?? null,
meetingLink: (e.meetingLink as string) ?? null,
hasSubmittedFeedback: (e.hasSubmittedFeedback as boolean) ?? false,
}
}
function mapInterviewSchedule(raw: unknown): AshbyInterviewSchedule {
const s = (raw ?? {}) as UnknownRecord
return {
id: (s.id as string) ?? '',
status: (s.status as string) ?? null,
applicationId: (s.applicationId as string) ?? '',
interviewStageId: (s.interviewStageId as string) ?? null,
scheduledBy: mapUserSummary(s.scheduledBy),
createdAt: (s.createdAt as string) ?? null,
updatedAt: (s.updatedAt as string) ?? null,
interviewEvents: Array.isArray(s.interviewEvents)
? (s.interviewEvents as unknown[])
.map(mapInterviewEvent)
.filter((e): e is AshbyInterviewEvent => e !== null)
: [],
}
}
export const listInterviewsTool: ToolConfig<
AshbyListInterviewSchedulesParams,
AshbyListInterviewSchedulesResponse
> = {
id: 'ashby_list_interviews',
name: 'Ashby List Interview Schedules',
description:
'Lists interview schedules in Ashby, optionally filtered by application or interview stage.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
applicationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The UUID of the application to list interview schedules for',
},
interviewStageId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The UUID of the interview stage to list interview schedules for',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return interview schedules created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
},
request: {
url: 'https://api.ashbyhq.com/interviewSchedule.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.applicationId) body.applicationId = params.applicationId.trim()
if (params.interviewStageId) body.interviewStageId = params.interviewStageId.trim()
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list interview schedules'))
}
return {
success: true,
output: {
interviewSchedules: (data.results ?? []).map(mapInterviewSchedule),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
interviewSchedules: {
type: 'array',
description: 'List of interview schedules',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Interview schedule UUID' },
status: {
type: 'string',
description:
'Schedule status (NeedsScheduling, WaitingOnCandidateBooking, Scheduled, Complete, Cancelled, OnHold, etc.)',
optional: true,
},
applicationId: { type: 'string', description: 'Associated application UUID' },
interviewStageId: {
type: 'string',
description: 'Interview stage UUID',
optional: true,
},
scheduledBy: {
...USER_SUMMARY_OUTPUT,
description: 'User who scheduled the interview (null if not yet scheduled)',
},
createdAt: {
type: 'string',
description: 'ISO 8601 creation timestamp',
optional: true,
},
updatedAt: {
type: 'string',
description: 'ISO 8601 last update timestamp',
optional: true,
},
interviewEvents: {
type: 'array',
description: 'Scheduled interview events on this schedule',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Event UUID' },
interviewId: {
type: 'string',
description: 'Interview template UUID',
optional: true,
},
interviewScheduleId: {
type: 'string',
description: 'Parent schedule UUID',
optional: true,
},
interviewerUserIds: {
type: 'array',
description: 'User UUIDs of interviewers assigned to the event',
items: { type: 'string', description: 'User UUID' },
},
createdAt: {
type: 'string',
description: 'Event creation timestamp',
optional: true,
},
updatedAt: {
type: 'string',
description: 'Event last updated timestamp',
optional: true,
},
startTime: {
type: 'string',
description: 'Event start time',
optional: true,
},
endTime: { type: 'string', description: 'Event end time', optional: true },
feedbackLink: {
type: 'string',
description: 'URL to submit feedback for the event',
optional: true,
},
location: {
type: 'string',
description: 'Physical location',
optional: true,
},
meetingLink: {
type: 'string',
description: 'Virtual meeting URL',
optional: true,
},
hasSubmittedFeedback: {
type: 'boolean',
description: 'Whether any feedback has been submitted',
},
},
},
},
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
+228
View File
@@ -0,0 +1,228 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListJobPostingsParams {
apiKey: string
location?: string
department?: string
listedOnly?: boolean
jobBoardId?: string
}
interface AshbyJobPostingSummary {
id: string
title: string
jobId: string | null
departmentName: string | null
teamName: string | null
locationName: string | null
locationIds: {
primaryLocationId: string | null
secondaryLocationIds: string[]
} | null
workplaceType: string | null
employmentType: string | null
isListed: boolean
publishedDate: string | null
applicationDeadline: string | null
externalLink: string | null
applyLink: string | null
compensationTierSummary: string | null
shouldDisplayCompensationOnJobBoard: boolean
updatedAt: string | null
}
interface AshbyListJobPostingsResponse extends ToolResponse {
output: {
jobPostings: AshbyJobPostingSummary[]
}
}
export const listJobPostingsTool: ToolConfig<
AshbyListJobPostingsParams,
AshbyListJobPostingsResponse
> = {
id: 'ashby_list_job_postings',
name: 'Ashby List Job Postings',
description: 'Lists all job postings in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by location name (case sensitive)',
},
department: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by department name (case sensitive)',
},
listedOnly: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, only returns listed (publicly visible) job postings (default false)',
},
jobBoardId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'UUID of a specific job board to filter postings to. If omitted, returns postings on the primary external job board.',
},
},
request: {
url: 'https://api.ashbyhq.com/jobPosting.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.location) body.location = params.location
if (params.department) body.department = params.department
if (params.listedOnly !== undefined) body.listedOnly = params.listedOnly
if (params.jobBoardId) body.jobBoardId = params.jobBoardId.trim()
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list job postings'))
}
return {
success: true,
output: {
jobPostings: (data.results ?? []).map(
(
jp: Record<string, unknown> & {
locationIds?: { primaryLocationId?: string; secondaryLocationIds?: string[] }
}
) => ({
id: (jp.id as string) ?? '',
title: (jp.title as string) ?? '',
jobId: (jp.jobId as string) ?? null,
departmentName: (jp.departmentName as string) ?? null,
teamName: (jp.teamName as string) ?? null,
locationName: (jp.locationName as string) ?? null,
locationIds: jp.locationIds
? {
primaryLocationId: jp.locationIds.primaryLocationId ?? null,
secondaryLocationIds: Array.isArray(jp.locationIds.secondaryLocationIds)
? jp.locationIds.secondaryLocationIds
: [],
}
: null,
workplaceType: (jp.workplaceType as string) ?? null,
employmentType: (jp.employmentType as string) ?? null,
isListed: (jp.isListed as boolean) ?? false,
publishedDate: (jp.publishedDate as string) ?? null,
applicationDeadline: (jp.applicationDeadline as string) ?? null,
externalLink: (jp.externalLink as string) ?? null,
applyLink: (jp.applyLink as string) ?? null,
compensationTierSummary: (jp.compensationTierSummary as string) ?? null,
shouldDisplayCompensationOnJobBoard:
(jp.shouldDisplayCompensationOnJobBoard as boolean) ?? false,
updatedAt: (jp.updatedAt as string) ?? null,
})
),
},
}
},
outputs: {
jobPostings: {
type: 'array',
description: 'List of job postings',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job posting UUID' },
title: { type: 'string', description: 'Job posting title' },
jobId: { type: 'string', description: 'Associated job UUID', optional: true },
departmentName: { type: 'string', description: 'Department name', optional: true },
teamName: { type: 'string', description: 'Team name', optional: true },
locationName: {
type: 'string',
description: 'Primary location display name',
optional: true,
},
locationIds: {
type: 'object',
description: 'Primary and secondary location UUIDs',
optional: true,
properties: {
primaryLocationId: {
type: 'string',
description: 'Primary location UUID',
optional: true,
},
secondaryLocationIds: {
type: 'array',
description: 'Secondary location UUIDs',
items: { type: 'string', description: 'Location UUID' },
},
},
},
workplaceType: {
type: 'string',
description: 'Workplace type (OnSite, Remote, Hybrid)',
optional: true,
},
employmentType: {
type: 'string',
description: 'Employment type (FullTime, PartTime, Intern, Contract, Temporary)',
optional: true,
},
isListed: { type: 'boolean', description: 'Whether the posting is publicly listed' },
publishedDate: {
type: 'string',
description: 'ISO 8601 published date',
optional: true,
},
applicationDeadline: {
type: 'string',
description: 'ISO 8601 application deadline',
optional: true,
},
externalLink: {
type: 'string',
description: 'External link to the job posting',
optional: true,
},
applyLink: {
type: 'string',
description: 'Direct apply link for the job posting',
optional: true,
},
compensationTierSummary: {
type: 'string',
description: 'Compensation tier summary for job boards',
optional: true,
},
shouldDisplayCompensationOnJobBoard: {
type: 'boolean',
description: 'Whether compensation is shown on the job board',
},
updatedAt: {
type: 'string',
description: 'ISO 8601 last update timestamp',
optional: true,
},
},
},
},
},
}
+143
View File
@@ -0,0 +1,143 @@
import type { AshbyListJobsParams, AshbyListJobsResponse } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage, JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const listJobsTool: ToolConfig<AshbyListJobsParams, AshbyListJobsResponse> = {
id: 'ashby_list_jobs',
name: 'Ashby List Jobs',
description:
'Lists all jobs in an Ashby organization. By default returns Open, Closed, and Archived jobs. Specify status to filter.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by job status: Open, Closed, Archived, or Draft',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return jobs created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
openedAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs opened after this ISO 8601 timestamp',
},
openedBefore: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs opened before this ISO 8601 timestamp',
},
closedAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs closed after this ISO 8601 timestamp',
},
closedBefore: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs closed before this ISO 8601 timestamp',
},
},
request: {
url: 'https://api.ashbyhq.com/job.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = { expand: ['openings', 'location'] }
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.status) body.status = [params.status]
const isoToMs = (iso: string): number | null => {
const ms = new Date(iso).getTime()
return Number.isNaN(ms) ? null : ms
}
if (params.createdAfter) {
const ms = isoToMs(params.createdAfter)
if (ms !== null) body.createdAfter = ms
}
if (params.openedAfter) {
const ms = isoToMs(params.openedAfter)
if (ms !== null) body.openedAfter = ms
}
if (params.openedBefore) {
const ms = isoToMs(params.openedBefore)
if (ms !== null) body.openedBefore = ms
}
if (params.closedAfter) {
const ms = isoToMs(params.closedAfter)
if (ms !== null) body.closedAfter = ms
}
if (params.closedBefore) {
const ms = isoToMs(params.closedBefore)
if (ms !== null) body.closedBefore = ms
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list jobs'))
}
return {
success: true,
output: {
jobs: (data.results ?? []).map(mapJob),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
jobs: {
type: 'array',
description: 'List of jobs',
items: {
type: 'object',
properties: JOB_OUTPUTS,
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
+225
View File
@@ -0,0 +1,225 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListLocationsParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
includeArchived?: boolean
includeLocationHierarchy?: boolean
}
interface AshbyLocation {
id: string
name: string
externalName: string | null
isArchived: boolean
isRemote: boolean
workplaceType: string | null
parentLocationId: string | null
type: string | null
address: {
addressCountry: string | null
addressRegion: string | null
addressLocality: string | null
postalCode: string | null
streetAddress: string | null
} | null
extraData: Record<string, unknown> | null
}
interface AshbyListLocationsResponse extends ToolResponse {
output: {
locations: AshbyLocation[]
moreDataAvailable: boolean
nextCursor: string | null
syncToken: string | null
}
}
export const listLocationsTool: ToolConfig<AshbyListLocationsParams, AshbyListLocationsResponse> = {
id: 'ashby_list_locations',
name: 'Ashby List Locations',
description: 'Lists all locations configured in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default and max 100)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived locations in results (default false)',
},
includeLocationHierarchy: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes location hierarchy components/regions (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/location.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.syncToken) body.syncToken = params.syncToken
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
if (params.includeLocationHierarchy !== undefined)
body.includeLocationHierarchy = params.includeLocationHierarchy
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list locations'))
}
return {
success: true,
output: {
locations: (data.results ?? []).map(
(
l: Record<string, unknown> & {
address?: { postalAddress?: Record<string, unknown> }
}
) => {
const pa = l.address?.postalAddress
return {
id: (l.id as string) ?? '',
name: (l.name as string) ?? '',
externalName: (l.externalName as string) ?? null,
isArchived: (l.isArchived as boolean) ?? false,
isRemote: (l.isRemote as boolean) ?? false,
workplaceType: (l.workplaceType as string) ?? null,
parentLocationId: (l.parentLocationId as string) ?? null,
type: (l.type as string) ?? null,
address: pa
? {
addressCountry: (pa.addressCountry as string) ?? null,
addressRegion: (pa.addressRegion as string) ?? null,
addressLocality: (pa.addressLocality as string) ?? null,
postalCode: (pa.postalCode as string) ?? null,
streetAddress: (pa.streetAddress as string) ?? null,
}
: null,
extraData: (l.extraData as Record<string, unknown>) ?? null,
}
}
),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
syncToken: data.syncToken ?? null,
},
}
},
outputs: {
locations: {
type: 'array',
description: 'List of locations',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Location UUID' },
name: { type: 'string', description: 'Location name' },
externalName: {
type: 'string',
description: 'Candidate-facing name used on job boards',
optional: true,
},
isArchived: { type: 'boolean', description: 'Whether the location is archived' },
isRemote: {
type: 'boolean',
description: 'Whether the location is remote (use workplaceType instead)',
},
workplaceType: {
type: 'string',
description: 'Workplace type (OnSite, Hybrid, Remote)',
optional: true,
},
parentLocationId: {
type: 'string',
description: 'Parent location UUID',
optional: true,
},
type: {
type: 'string',
description: 'Location component type (Location, LocationHierarchy)',
optional: true,
},
address: {
type: 'object',
description: 'Location 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 },
},
},
extraData: {
type: 'json',
description: 'Free-form key-value metadata',
optional: true,
},
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
syncToken: {
type: 'string',
description: 'Opaque sync token returned after the last page; pass on next sync',
optional: true,
},
},
}
+148
View File
@@ -0,0 +1,148 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListNotesParams {
apiKey: string
candidateId: string
cursor?: string
perPage?: number
}
interface AshbyListNotesResponse extends ToolResponse {
output: {
notes: Array<{
id: string
content: string | null
isPrivate: boolean
author: {
id: string
firstName: string | null
lastName: string | null
email: string | null
} | null
createdAt: string | null
}>
moreDataAvailable: boolean
nextCursor: string | null
}
}
export const listNotesTool: ToolConfig<AshbyListNotesParams, AshbyListNotesResponse> = {
id: 'ashby_list_notes',
name: 'Ashby List Notes',
description: 'Lists all notes on a candidate with pagination support.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
candidateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the candidate to list notes for',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.listNotes',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list notes'))
}
return {
success: true,
output: {
notes: (data.results ?? []).map(
(
n: Record<string, unknown> & {
author?: { id?: string; firstName?: string; lastName?: string; email?: string }
}
) => ({
id: (n.id as string) ?? '',
content: (n.content as string) ?? null,
isPrivate: (n.isPrivate as boolean) ?? false,
author: n.author
? {
id: n.author.id ?? '',
firstName: n.author.firstName ?? null,
lastName: n.author.lastName ?? null,
email: n.author.email ?? null,
}
: null,
createdAt: (n.createdAt as string) ?? null,
})
),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
notes: {
type: 'array',
description: 'List of notes on the candidate',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Note UUID' },
content: { type: 'string', description: 'Note content', optional: true },
isPrivate: { type: 'boolean', description: 'Whether the note is private' },
author: {
type: 'object',
description: 'Note author',
optional: true,
properties: {
id: { type: 'string', description: 'Author user UUID' },
firstName: { type: 'string', description: 'First name', optional: true },
lastName: { type: 'string', description: 'Last name', optional: true },
email: { type: 'string', description: 'Email address', optional: true },
},
},
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true },
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
+122
View File
@@ -0,0 +1,122 @@
import type { AshbyOffer } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage, mapOffer, OFFER_OUTPUTS } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListOffersParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
createdAfter?: string
applicationId?: string
}
interface AshbyListOffersResponse extends ToolResponse {
output: {
offers: AshbyOffer[]
moreDataAvailable: boolean
nextCursor: string | null
}
}
export const listOffersTool: ToolConfig<AshbyListOffersParams, AshbyListOffersResponse> = {
id: 'ashby_list_offers',
name: 'Ashby List Offers',
description: 'Lists all offers with their latest version in an Ashby organization.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return offers created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
applicationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return only offers for the specified application UUID',
},
},
request: {
url: 'https://api.ashbyhq.com/offer.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
if (params.syncToken) body.syncToken = params.syncToken
if (params.applicationId) body.applicationId = params.applicationId.trim()
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list offers'))
}
return {
success: true,
output: {
offers: (data.results ?? []).map(mapOffer),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
offers: {
type: 'array',
description: 'List of offers',
items: {
type: 'object',
properties: OFFER_OUTPUTS,
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
+104
View File
@@ -0,0 +1,104 @@
import type { AshbyOpening } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
mapOpenings,
OPENINGS_OUTPUT,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListOpeningsParams {
apiKey: string
cursor?: string
perPage?: number
createdAfter?: string
}
interface AshbyListOpeningsResponse extends ToolResponse {
output: {
openings: AshbyOpening[]
moreDataAvailable: boolean
nextCursor: string | null
}
}
export const listOpeningsTool: ToolConfig<AshbyListOpeningsParams, AshbyListOpeningsResponse> = {
id: 'ashby_list_openings',
name: 'Ashby List Openings',
description: 'Lists all openings in Ashby with pagination.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return openings created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
},
request: {
url: 'https://api.ashbyhq.com/opening.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list openings'))
}
return {
success: true,
output: {
openings: mapOpenings(data.results),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
openings: OPENINGS_OUTPUT,
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
+103
View File
@@ -0,0 +1,103 @@
import type { AshbySourceSummary } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListSourcesParams {
apiKey: string
includeArchived?: boolean
}
interface AshbyListSourcesResponse extends ToolResponse {
output: {
sources: AshbySourceSummary[]
}
}
export const listSourcesTool: ToolConfig<AshbyListSourcesParams, AshbyListSourcesResponse> = {
id: 'ashby_list_sources',
name: 'Ashby List Sources',
description: 'Lists all candidate sources configured in Ashby.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived sources in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/source.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list sources'))
}
return {
success: true,
output: {
sources: (data.results ?? []).map(
(s: Record<string, unknown> & { sourceType?: Record<string, unknown> }) => {
const sourceType = s.sourceType
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,
}
}
),
},
}
},
outputs: {
sources: {
type: 'array',
description: 'List of sources',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Source UUID' },
title: { type: 'string', description: 'Source title' },
isArchived: { type: 'boolean', description: 'Whether the source is 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' },
},
},
},
},
},
},
}
+110
View File
@@ -0,0 +1,110 @@
import type { AshbyUserSummary } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
mapUserSummary,
USER_SUMMARY_OUTPUT,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListUsersParams {
apiKey: string
cursor?: string
perPage?: number
includeDeactivated?: boolean
}
interface AshbyListUsersResponse extends ToolResponse {
output: {
users: AshbyUserSummary[]
moreDataAvailable: boolean
nextCursor: string | null
}
}
export const listUsersTool: ToolConfig<AshbyListUsersParams, AshbyListUsersResponse> = {
id: 'ashby_list_users',
name: 'Ashby List Users',
description: 'Lists all users in Ashby with pagination.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
includeDeactivated: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes deactivated users in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/user.list',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.includeDeactivated !== undefined)
body.includeDeactivated = params.includeDeactivated
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to list users'))
}
return {
success: true,
output: {
users: (data.results ?? [])
.map(mapUserSummary)
.filter((u: AshbyUserSummary | null): u is AshbyUserSummary => u !== null),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
users: {
type: 'array',
description: 'List of users',
items: {
type: 'object',
properties: USER_SUMMARY_OUTPUT.properties,
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
},
}
@@ -0,0 +1,74 @@
import type { AshbyCandidate } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyRemoveCandidateTagParams {
apiKey: string
candidateId: string
tagId: string
}
interface AshbyRemoveCandidateTagResponse extends ToolResponse {
output: AshbyCandidate
}
export const removeCandidateTagTool: ToolConfig<
AshbyRemoveCandidateTagParams,
AshbyRemoveCandidateTagResponse
> = {
id: 'ashby_remove_candidate_tag',
name: 'Ashby Remove Candidate Tag',
description: 'Removes a tag from a candidate in Ashby and returns the updated candidate.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
candidateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the candidate to remove the tag from',
},
tagId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the tag to remove',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.removeTag',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
candidateId: params.candidateId.trim(),
tagId: params.tagId.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to remove tag from candidate'))
}
return {
success: true,
output: mapCandidate(data.results),
}
},
outputs: CANDIDATE_OUTPUTS,
}
+81
View File
@@ -0,0 +1,81 @@
import type {
AshbySearchCandidatesParams,
AshbySearchCandidatesResponse,
} from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const searchCandidatesTool: ToolConfig<
AshbySearchCandidatesParams,
AshbySearchCandidatesResponse
> = {
id: 'ashby_search_candidates',
name: 'Ashby Search Candidates',
description:
'Searches for candidates by name and/or email with AND logic. Results are limited to 100 matches. Use candidate.list for full pagination.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Candidate name to search for (combined with email using AND logic)',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Candidate email to search for (combined with name using AND logic)',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.search',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.name) body.name = params.name
if (params.email) body.email = params.email
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to search candidates'))
}
return {
success: true,
output: {
candidates: (data.results ?? []).map(mapCandidate),
},
}
},
outputs: {
candidates: {
type: 'array',
description: 'Matching candidates (max 100 results)',
items: {
type: 'object',
properties: CANDIDATE_OUTPUTS,
},
},
},
}
+380
View File
@@ -0,0 +1,380 @@
import type { ToolResponse } from '@/tools/types'
interface AshbyBaseParams {
apiKey: string
}
export interface AshbyContactInfo {
value: string
type: string
isPrimary: boolean
}
interface AshbySocialLink {
type: string
url: string
}
interface AshbyTag {
id: string
title: string
isArchived: boolean
}
export interface AshbyFileHandle {
id: string
name: string
handle: string
}
export interface AshbyCustomField {
id: string | null
title: string
isPrivate: boolean
valueLabel: string | null
value: unknown
}
export interface AshbyUserSummary {
id: string
firstName: string | null
lastName: string | null
email: string | null
globalRole: string | null
isEnabled: boolean
updatedAt: string | null
managerId: string | null
}
export interface AshbySourceSummary {
id: string
title: string
isArchived: boolean
sourceType: {
id: string
title: string
isArchived: boolean
} | null
}
interface AshbyCandidateLocation {
id: string | null
locationSummary: string | null
locationComponents: Array<{ type: string; name: string }>
}
export interface AshbyCandidate {
id: string
name: string
primaryEmailAddress: AshbyContactInfo | null
primaryPhoneNumber: AshbyContactInfo | null
emailAddresses: AshbyContactInfo[]
phoneNumbers: AshbyContactInfo[]
socialLinks: AshbySocialLink[]
linkedInUrl: string | null
githubUrl: string | null
profileUrl: string | null
position: string | null
company: string | null
school: string | null
timezone: string | null
location: AshbyCandidateLocation | null
tags: AshbyTag[]
applicationIds: string[]
customFields: AshbyCustomField[]
resumeFileHandle: AshbyFileHandle | null
fileHandles: AshbyFileHandle[]
source: AshbySourceSummary | null
creditedToUser: AshbyUserSummary | null
fraudStatus: string | null
createdAt: string | null
updatedAt: string | null
}
export interface AshbyListCandidatesParams extends AshbyBaseParams {
cursor?: string
perPage?: number
createdAfter?: string
}
export interface AshbyGetCandidateParams extends AshbyBaseParams {
candidateId: string
}
export interface AshbyCreateCandidateParams extends AshbyBaseParams {
name: string
email?: string
phoneNumber?: string
linkedInUrl?: string
githubUrl?: string
website?: string
sourceId?: string
creditedToUserId?: string
createdAt?: string
alternateEmailAddresses?: string[]
}
export interface AshbySearchCandidatesParams extends AshbyBaseParams {
name?: string
email?: string
}
export interface AshbyListJobsParams extends AshbyBaseParams {
cursor?: string
perPage?: number
status?: string
createdAfter?: string
openedAfter?: string
openedBefore?: string
closedAfter?: string
closedBefore?: string
}
export interface AshbyGetJobParams extends AshbyBaseParams {
jobId: string
}
export interface AshbyCreateNoteParams extends AshbyBaseParams {
candidateId: string
note: string
noteType?: string
sendNotifications?: boolean
isPrivate?: boolean
createdAt?: string
}
export interface AshbyListApplicationsParams extends AshbyBaseParams {
cursor?: string
perPage?: number
status?: string
jobId?: string
createdAfter?: string
}
export interface AshbyListCandidatesResponse extends ToolResponse {
output: {
candidates: AshbyCandidate[]
moreDataAvailable: boolean
nextCursor: string | null
}
}
export interface AshbyGetCandidateResponse extends ToolResponse {
output: AshbyCandidate
}
export interface AshbyCreateCandidateResponse extends ToolResponse {
output: AshbyCandidate
}
export interface AshbySearchCandidatesResponse extends ToolResponse {
output: {
candidates: AshbyCandidate[]
}
}
interface AshbyJobLocation {
id: string | null
name: string | null
externalName: string | null
isArchived: boolean
isRemote: boolean
workplaceType: string | null
parentLocationId: string | null
type: string | null
address: {
addressCountry: string | null
addressRegion: string | null
addressLocality: string | null
postalCode: string | null
streetAddress: string | null
} | null
}
export interface AshbyHiringTeamMember {
email: string | null
firstName: string | null
lastName: string | null
role: string | null
userId: string | null
}
export interface AshbyOpeningLatestVersion {
id: string | null
identifier: string | null
description: string | null
authorId: string | null
createdAt: string | null
teamId: string | null
jobIds: string[]
targetHireDate: string | null
targetStartDate: string | null
isBackfill: boolean
employmentType: string | null
locationIds: string[]
hiringTeam: AshbyHiringTeamMember[]
customFields: AshbyCustomField[]
}
export interface AshbyOpening {
id: string
openedAt: string | null
closedAt: string | null
isArchived: boolean
archivedAt: string | null
closeReasonId: string | null
openingState: string | null
latestVersion: AshbyOpeningLatestVersion | null
}
interface AshbyJobCompensationTier {
id: string | null
title: string | null
additionalInformation: string | null
tierSummary: string | null
}
export interface AshbyJob {
id: string
title: string
confidential: boolean
status: string | null
employmentType: string | null
locationId: string | null
departmentId: string | null
defaultInterviewPlanId: string | null
interviewPlanIds: string[]
customFields: AshbyCustomField[]
jobPostingIds: string[]
customRequisitionId: string | null
brandId: string | null
hiringTeam: AshbyHiringTeamMember[]
author: AshbyUserSummary | null
createdAt: string | null
updatedAt: string | null
openedAt: string | null
closedAt: string | null
location: AshbyJobLocation | null
openings: AshbyOpening[]
compensation: {
compensationTiers: AshbyJobCompensationTier[]
} | null
}
export interface AshbyListJobsResponse extends ToolResponse {
output: {
jobs: AshbyJob[]
moreDataAvailable: boolean
nextCursor: string | null
}
}
export interface AshbyGetJobResponse extends ToolResponse {
output: AshbyJob
}
interface AshbyNote {
id: string
createdAt: string | null
isPrivate: boolean
content: string | null
author: {
id: string
firstName: string | null
lastName: string | null
email: string | null
} | null
}
export interface AshbyCreateNoteResponse extends ToolResponse {
output: AshbyNote
}
interface AshbyApplicationCandidate {
id: string
name: string | null
primaryEmailAddress: AshbyContactInfo | null
primaryPhoneNumber: AshbyContactInfo | null
}
interface AshbyApplicationJob {
id: string
title: string | null
locationId: string | null
departmentId: string | null
}
interface AshbyApplicationStage {
id: string
title: string | null
type: string | null
orderInInterviewPlan: number | null
interviewStageGroupId: string | null
interviewPlanId: string | null
}
interface AshbyApplicationArchiveReason {
id: string
text: string | null
reasonType: string | null
isArchived: boolean
customFields: AshbyCustomField[]
}
interface AshbyApplicationHistoryEntry {
id: string
stageId: string | null
stageNumber: number | null
title: string | null
enteredStageAt: string | null
actorId: string | null
}
export interface AshbyApplication {
id: string
createdAt: string | null
updatedAt: string | null
status: string
customFields: AshbyCustomField[]
candidate: AshbyApplicationCandidate
currentInterviewStage: AshbyApplicationStage | null
source: AshbySourceSummary | null
archiveReason: AshbyApplicationArchiveReason | null
archivedAt: string | null
job: AshbyApplicationJob
creditedToUser: AshbyUserSummary | null
hiringTeam: AshbyHiringTeamMember[]
appliedViaJobPostingId: string | null
submitterClientIp: string | null
submitterUserAgent: string | null
applicationHistory: AshbyApplicationHistoryEntry[]
}
export interface AshbyListApplicationsResponse extends ToolResponse {
output: {
applications: AshbyApplication[]
moreDataAvailable: boolean
nextCursor: string | null
}
}
export interface AshbyOfferVersion {
id: string | null
startDate: string | null
salary: { currencyCode: string | null; value: number | null } | null
createdAt: string | null
openingId: string | null
customFields: AshbyCustomField[]
fileHandles: AshbyFileHandle[]
author: AshbyUserSummary | null
approvalStatus: string | null
}
export interface AshbyOffer {
id: string
decidedAt: string | null
applicationId: string | null
acceptanceStatus: string | null
offerStatus: string | null
latestVersion: AshbyOfferVersion | null
}
+164
View File
@@ -0,0 +1,164 @@
import type { AshbyGetCandidateResponse } from '@/tools/ashby/types'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
interface AshbyUpdateCandidateParams {
apiKey: string
candidateId: string
name?: string
email?: string
phoneNumber?: string
linkedInUrl?: string
githubUrl?: string
websiteUrl?: string
alternateEmail?: string
sourceId?: string
creditedToUserId?: string
createdAt?: string
sendNotifications?: boolean
socialLinks?: Array<{ type: string; url: string }>
}
export const updateCandidateTool: ToolConfig<
AshbyUpdateCandidateParams,
AshbyGetCandidateResponse
> = {
id: 'ashby_update_candidate',
name: 'Ashby Update Candidate',
description: 'Updates an existing candidate record in Ashby. Only provided fields are changed.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Ashby API Key',
},
candidateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the candidate to update',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Updated full name',
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Updated primary email address',
},
phoneNumber: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Updated primary phone number',
},
linkedInUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'LinkedIn profile URL',
},
githubUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'GitHub profile URL',
},
websiteUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Personal website URL',
},
alternateEmail: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'An additional email address to add to the candidate',
},
sourceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the source to attribute the candidate to',
},
creditedToUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the Ashby user to credit with sourcing this candidate',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Backdated creation timestamp in ISO 8601. Only updatable if originally backdated.',
},
sendNotifications: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to send a notification when the source is updated (default true)',
},
socialLinks: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Array of social link objects to set on the candidate, e.g. [{"type":"LinkedIn","url":"https://..."}]. Replaces existing social links.',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.update',
method: 'POST',
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
}
if (params.name) body.name = params.name
if (params.email) body.email = params.email
if (params.phoneNumber) body.phoneNumber = params.phoneNumber
if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl
if (params.githubUrl) body.githubUrl = params.githubUrl
if (params.websiteUrl) body.websiteUrl = params.websiteUrl
if (params.alternateEmail) body.alternateEmail = params.alternateEmail
if (params.sourceId) body.sourceId = params.sourceId.trim()
if (params.creditedToUserId) body.creditedToUserId = params.creditedToUserId.trim()
if (params.createdAt) body.createdAt = params.createdAt
if (params.sendNotifications !== undefined) body.sendNotifications = params.sendNotifications
if (Array.isArray(params.socialLinks) && params.socialLinks.length > 0)
body.socialLinks = params.socialLinks
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(ashbyErrorMessage(data, 'Failed to update candidate'))
}
return {
success: true,
output: mapCandidate(data.results),
}
},
outputs: CANDIDATE_OUTPUTS,
}
+913
View File
@@ -0,0 +1,913 @@
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>