Skip to content

Commit a741fbc

Browse files
waleedlatif1claude
andcommitted
refactor(ashby): align tools, block, and triggers with Ashby API
Audit-driven refactor to destructure rich fields per Ashby's API docs, centralize output shapes via shared mappers in tools/ashby/utils.ts, and align webhook provider handler with trigger IDs via a shared action map. Removes stale block outputs left over from prior flat response shapes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 04f1d01 commit a741fbc

36 files changed

Lines changed: 2803 additions & 1352 deletions

apps/docs/content/docs/en/tools/ashby.mdx

Lines changed: 440 additions & 187 deletions
Large diffs are not rendered by default.

apps/docs/content/docs/en/triggers/ashby.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ Trigger workflow when a candidate is hired
9797
|`job` | object | job output from the tool |
9898
|`id` | string | Job UUID |
9999
|`title` | string | Job title |
100+
| `offer` | object | offer output from the tool |
101+
|`id` | string | Accepted offer UUID |
102+
|`applicationId` | string | Associated application UUID |
103+
|`acceptanceStatus` | string | Offer acceptance status |
104+
|`offerStatus` | string | Offer process status |
105+
|`decidedAt` | string | Offer decision timestamp \(ISO 8601\) |
106+
|`latestVersion` | object | latestVersion output from the tool |
107+
|`id` | string | Latest offer version UUID |
100108

101109

102110
---

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@
10311031
},
10321032
{
10331033
"name": "List Applications",
1034-
"description": "Lists all applications in an Ashby organization with pagination and optional filters for status, job, candidate, and creation date."
1034+
"description": "Lists all applications in an Ashby organization with pagination and optional filters for status, job, and creation date."
10351035
},
10361036
{
10371037
"name": "Get Application",
@@ -1051,11 +1051,11 @@
10511051
},
10521052
{
10531053
"name": "Add Candidate Tag",
1054-
"description": "Adds a tag to a candidate in Ashby."
1054+
"description": "Adds a tag to a candidate in Ashby and returns the updated candidate."
10551055
},
10561056
{
10571057
"name": "Remove Candidate Tag",
1058-
"description": "Removes a tag from a candidate in Ashby."
1058+
"description": "Removes a tag from a candidate in Ashby and returns the updated candidate."
10591059
},
10601060
{
10611061
"name": "Get Offer",

apps/sim/blocks/blocks/ashby.ts

Lines changed: 111 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,6 @@ Output only the ISO 8601 timestamp string, nothing else.`,
308308
condition: { field: 'operation', value: 'list_applications' },
309309
mode: 'advanced',
310310
},
311-
{
312-
id: 'filterCandidateId',
313-
title: 'Candidate ID Filter',
314-
type: 'short-input',
315-
placeholder: 'Filter by candidate UUID',
316-
condition: { field: 'operation', value: 'list_applications' },
317-
mode: 'advanced',
318-
},
319311
{
320312
id: 'createdAfter',
321313
title: 'Created After',
@@ -366,6 +358,7 @@ Output only the ISO 8601 timestamp string, nothing else.`,
366358
'list_openings',
367359
'list_users',
368360
'list_interviews',
361+
'list_candidate_tags',
369362
],
370363
},
371364
mode: 'advanced',
@@ -386,10 +379,43 @@ Output only the ISO 8601 timestamp string, nothing else.`,
386379
'list_openings',
387380
'list_users',
388381
'list_interviews',
382+
'list_candidate_tags',
389383
],
390384
},
391385
mode: 'advanced',
392386
},
387+
{
388+
id: 'syncToken',
389+
title: 'Sync Token',
390+
type: 'short-input',
391+
placeholder: 'Sync token for incremental updates',
392+
condition: { field: 'operation', value: 'list_candidate_tags' },
393+
mode: 'advanced',
394+
},
395+
{
396+
id: 'includeArchived',
397+
title: 'Include Archived',
398+
type: 'switch',
399+
condition: {
400+
field: 'operation',
401+
value: ['list_candidate_tags', 'list_archive_reasons'],
402+
},
403+
mode: 'advanced',
404+
},
405+
{
406+
id: 'expandApplicationFormDefinition',
407+
title: 'Include Application Form Definition',
408+
type: 'switch',
409+
condition: { field: 'operation', value: 'get_job_posting' },
410+
mode: 'advanced',
411+
},
412+
{
413+
id: 'expandSurveyFormDefinitions',
414+
title: 'Include Survey Form Definitions',
415+
type: 'switch',
416+
condition: { field: 'operation', value: 'get_job_posting' },
417+
mode: 'advanced',
418+
},
393419
{
394420
id: 'tagId',
395421
title: 'Tag ID',
@@ -476,11 +502,25 @@ Output only the ISO 8601 timestamp string, nothing else.`,
476502
if (params.searchEmail) result.email = params.searchEmail
477503
if (params.filterStatus) result.status = params.filterStatus
478504
if (params.filterJobId) result.jobId = params.filterJobId
479-
if (params.filterCandidateId) result.candidateId = params.filterCandidateId
480505
if (params.jobStatus) result.status = params.jobStatus
481506
if (params.sendNotifications === 'true' || params.sendNotifications === true) {
482507
result.sendNotifications = true
483508
}
509+
if (params.includeArchived === 'true' || params.includeArchived === true) {
510+
result.includeArchived = true
511+
}
512+
if (
513+
params.expandApplicationFormDefinition === 'true' ||
514+
params.expandApplicationFormDefinition === true
515+
) {
516+
result.expandApplicationFormDefinition = true
517+
}
518+
if (
519+
params.expandSurveyFormDefinitions === 'true' ||
520+
params.expandSurveyFormDefinitions === true
521+
) {
522+
result.expandSurveyFormDefinitions = true
523+
}
484524
if (params.appCandidateId) result.candidateId = params.appCandidateId
485525
if (params.appCreatedAt) result.createdAt = params.appCreatedAt
486526
if (params.updateName) result.name = params.updateName
@@ -515,11 +555,20 @@ Output only the ISO 8601 timestamp string, nothing else.`,
515555
sendNotifications: { type: 'boolean', description: 'Send notifications' },
516556
filterStatus: { type: 'string', description: 'Application status filter' },
517557
filterJobId: { type: 'string', description: 'Job UUID filter' },
518-
filterCandidateId: { type: 'string', description: 'Candidate UUID filter' },
519558
createdAfter: { type: 'string', description: 'Filter by creation date' },
520559
jobStatus: { type: 'string', description: 'Job status filter' },
521560
cursor: { type: 'string', description: 'Pagination cursor' },
522561
perPage: { type: 'number', description: 'Results per page' },
562+
syncToken: { type: 'string', description: 'Sync token for incremental updates' },
563+
includeArchived: { type: 'boolean', description: 'Include archived records' },
564+
expandApplicationFormDefinition: {
565+
type: 'boolean',
566+
description: 'Include application form definition in job posting',
567+
},
568+
expandSurveyFormDefinitions: {
569+
type: 'boolean',
570+
description: 'Include survey form definitions in job posting',
571+
},
523572
tagId: { type: 'string', description: 'Tag UUID' },
524573
offerId: { type: 'string', description: 'Offer UUID' },
525574
jobPostingId: { type: 'string', description: 'Job posting UUID' },
@@ -530,93 +579,114 @@ Output only the ISO 8601 timestamp string, nothing else.`,
530579
candidates: {
531580
type: 'json',
532581
description:
533-
'List of candidates (id, name, primaryEmailAddress, primaryPhoneNumber, createdAt, updatedAt)',
582+
'List of candidates with rich fields (id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses[], phoneNumbers[], socialLinks[], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents[], tags[], applicationIds[], customFields[], resumeFileHandle, fileHandles[], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt)',
534583
},
535584
jobs: {
536585
type: 'json',
537586
description:
538-
'List of jobs (id, title, status, employmentType, departmentId, locationId, createdAt, updatedAt)',
587+
'List of jobs (id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds[], customFields[], jobPostingIds[], customRequisitionId, brandId, hiringTeam[], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings[] with latestVersion, compensation with compensationTiers[])',
539588
},
540589
applications: {
541590
type: 'json',
542591
description:
543-
'List of applications (id, status, candidate, job, currentInterviewStage, source, createdAt, updatedAt)',
592+
'List of applications (id, status, customFields[], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields[], archivedAt, job summary, creditedToUser, hiringTeam[], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt)',
544593
},
545594
notes: {
546595
type: 'json',
547-
description: 'List of notes (id, content, author, createdAt)',
596+
description: 'List of notes (id, content, author, isPrivate, createdAt)',
548597
},
549598
offers: {
550599
type: 'json',
551600
description:
552-
'List of offers (id, offerStatus, acceptanceStatus, applicationId, startDate, salary, openingId)',
601+
'List of offers (id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields[]/fileHandles[]/author/approvalStatus)',
553602
},
554603
archiveReasons: {
555604
type: 'json',
556-
description: 'List of archive reasons (id, text, reasonType, isArchived)',
605+
description:
606+
'List of archive reasons (id, text, reasonType [RejectedByCandidate/RejectedByOrg/Other], isArchived)',
557607
},
558608
sources: {
559609
type: 'json',
560-
description: 'List of sources (id, title, isArchived)',
610+
description: 'List of sources (id, title, isArchived, sourceType {id, title, isArchived})',
561611
},
562612
customFields: {
563613
type: 'json',
564-
description: 'List of custom fields (id, title, fieldType, objectType, isArchived)',
614+
description:
615+
'List of custom field definitions (id, title, isPrivate, fieldType, objectType, isArchived, isRequired, selectableValues[] {label, value, isArchived})',
565616
},
566617
departments: {
567618
type: 'json',
568-
description: 'List of departments (id, name, isArchived, parentId)',
619+
description:
620+
'List of departments (id, name, externalName, isArchived, parentId, createdAt, updatedAt)',
569621
},
570622
locations: {
571623
type: 'json',
572-
description: 'List of locations (id, name, isArchived, isRemote, address)',
624+
description:
625+
'List of locations (id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress)',
573626
},
574627
jobPostings: {
575628
type: 'json',
576629
description:
577-
'List of job postings (id, title, jobId, locationName, departmentName, employmentType, isListed, publishedDate)',
630+
'List of job postings (id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt)',
578631
},
579632
openings: {
580633
type: 'json',
581-
description: 'List of openings (id, openingState, isArchived, openedAt, closedAt)',
634+
description:
635+
'List of openings (id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds[]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds[]/hiringTeam[]/customFields[])',
582636
},
583637
users: {
584638
type: 'json',
585-
description: 'List of users (id, firstName, lastName, email, isEnabled, globalRole)',
639+
description:
640+
'List of users (id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId)',
586641
},
587642
interviewSchedules: {
588643
type: 'json',
589644
description:
590-
'List of interview schedules (id, applicationId, interviewStageId, status, createdAt)',
645+
'List of interview schedules (id, applicationId, interviewStageId, interviewEvents[] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt)',
591646
},
592647
tags: {
593648
type: 'json',
594649
description: 'List of candidate tags (id, title, isArchived)',
595650
},
596-
stageId: { type: 'string', description: 'Interview stage UUID after stage change' },
597-
success: { type: 'boolean', description: 'Whether the operation succeeded' },
598-
offerStatus: {
599-
type: 'string',
600-
description: 'Offer status (e.g. WaitingOnCandidateResponse, CandidateAccepted)',
651+
id: { type: 'string', description: 'Resource UUID' },
652+
name: { type: 'string', description: 'Resource name' },
653+
title: { type: 'string', description: 'Job title or job posting title' },
654+
status: { type: 'string', description: 'Status' },
655+
candidate: {
656+
type: 'json',
657+
description:
658+
'Candidate details (id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses[], phoneNumbers[], socialLinks[], customFields[], source, creditedToUser, createdAt, updatedAt)',
659+
},
660+
job: {
661+
type: 'json',
662+
description:
663+
'Job details (id, title, status, employmentType, locationId, departmentId, hiringTeam[], author, location, openings[], compensation, createdAt, updatedAt)',
601664
},
602-
acceptanceStatus: {
603-
type: 'string',
604-
description: 'Acceptance status (e.g. Accepted, Declined, Pending)',
665+
application: {
666+
type: 'json',
667+
description:
668+
'Application details (id, status, customFields[], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam[], createdAt, updatedAt)',
605669
},
606-
applicationId: { type: 'string', description: 'Associated application UUID' },
607-
openingId: { type: 'string', description: 'Opening UUID associated with the offer' },
608-
salary: {
670+
offer: {
609671
type: 'json',
610-
description: 'Salary details from latest version (currencyCode, value)',
672+
description:
673+
'Offer details (id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion)',
674+
},
675+
jobPosting: {
676+
type: 'json',
677+
description:
678+
'Job posting details (id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy[], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt)',
611679
},
612-
startDate: { type: 'string', description: 'Offer start date from latest version' },
613-
id: { type: 'string', description: 'Resource UUID' },
614-
name: { type: 'string', description: 'Resource name' },
615-
title: { type: 'string', description: 'Job title' },
616-
status: { type: 'string', description: 'Status' },
617680
noteId: { type: 'string', description: 'Created note UUID' },
618681
content: { type: 'string', description: 'Note content' },
682+
author: {
683+
type: 'json',
684+
description: 'Note author (id, firstName, lastName, email, globalRole, isEnabled)',
685+
},
686+
isPrivate: { type: 'boolean', description: 'Whether the note is private' },
687+
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
619688
moreDataAvailable: { type: 'boolean', description: 'Whether more pages exist' },
620689
nextCursor: { type: 'string', description: 'Pagination cursor for next page' },
690+
syncToken: { type: 'string', description: 'Sync token for incremental updates' },
621691
},
622692
}

apps/sim/lib/webhooks/providers/ashby.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { generateId } from '@sim/utils/id'
55
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
66
import type {
77
DeleteSubscriptionContext,
8+
EventMatchContext,
89
FormatInputContext,
910
FormatInputResult,
1011
SubscriptionContext,
@@ -48,7 +49,6 @@ export const ashbyHandler: WebhookProviderHandler = {
4849
input: {
4950
...((b.data as Record<string, unknown>) || {}),
5051
action: b.action,
51-
data: b.data || {},
5252
},
5353
}
5454
},
@@ -60,6 +60,34 @@ export const ashbyHandler: WebhookProviderHandler = {
6060
providerLabel: 'Ashby',
6161
}),
6262

63+
async matchEvent({
64+
webhook,
65+
body,
66+
requestId,
67+
providerConfig,
68+
}: EventMatchContext): Promise<boolean> {
69+
const triggerId = providerConfig.triggerId as string | undefined
70+
const obj = body as Record<string, unknown>
71+
const action = typeof obj?.action === 'string' ? obj.action : ''
72+
73+
if (!triggerId) return true
74+
75+
const { isAshbyEventMatch } = await import('@/triggers/ashby/utils')
76+
if (!isAshbyEventMatch(triggerId, action)) {
77+
logger.debug(
78+
`[${requestId}] Ashby event mismatch for trigger ${triggerId}. Action: ${action || '(missing)'}. Skipping execution.`,
79+
{
80+
webhookId: webhook.id,
81+
triggerId,
82+
receivedAction: action,
83+
}
84+
)
85+
return false
86+
}
87+
88+
return true
89+
},
90+
6391
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
6492
try {
6593
const providerConfig = getProviderConfig(ctx.webhook)
@@ -78,18 +106,12 @@ export const ashbyHandler: WebhookProviderHandler = {
78106
throw new Error('Trigger ID is required to create Ashby webhook.')
79107
}
80108

81-
const webhookTypeMap: Record<string, string> = {
82-
ashby_application_submit: 'applicationSubmit',
83-
ashby_candidate_stage_change: 'candidateStageChange',
84-
ashby_candidate_hire: 'candidateHire',
85-
ashby_candidate_delete: 'candidateDelete',
86-
ashby_job_create: 'jobCreate',
87-
ashby_offer_create: 'offerCreate',
88-
}
89-
90-
const webhookType = webhookTypeMap[triggerId]
109+
const { ASHBY_TRIGGER_ACTION_MAP } = await import('@/triggers/ashby/utils')
110+
const webhookType = ASHBY_TRIGGER_ACTION_MAP[triggerId]
91111
if (!webhookType) {
92-
throw new Error(`Unknown Ashby triggerId: ${triggerId}. Add it to webhookTypeMap.`)
112+
throw new Error(
113+
`Unknown Ashby triggerId: ${triggerId}. Add it to ASHBY_TRIGGER_ACTION_MAP.`
114+
)
93115
}
94116

95117
const notificationUrl = getNotificationUrl(ctx.webhook)

0 commit comments

Comments
 (0)