Skip to content

Commit df833ef

Browse files
authored
feat(ashby): expand hiring operations and webhooks (#7341)
* feat(ashby): expand hiring operations and webhooks * test(ashby): type webhook fixtures * fix(ashby): harden uploads and pagination * fix(ashby): correct API contracts and webhook handling
1 parent f59c69f commit df833ef

79 files changed

Lines changed: 4913 additions & 769 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/integrations/ashby.mdx

Lines changed: 910 additions & 93 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/ashby.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ describe('AshbyBlock', () => {
4747
expect(result.socialLinks).toEqual([{ type: 'Twitter', url: 'https://twitter.com/jane' }])
4848
})
4949

50+
it('preserves an empty JSON array so users can clear all social links', () => {
51+
const result = AshbyBlock.tools.config.params!(
52+
buildParams('update_candidate', { socialLinks: '[]' })
53+
)
54+
expect(result.socialLinks).toEqual([])
55+
})
56+
5057
it('throws instead of silently dropping the field when the JSON is malformed', () => {
5158
// A silent [] here would let the Ashby update proceed without applying
5259
// the requested links and with no error shown to the workflow author.
@@ -66,6 +73,43 @@ describe('AshbyBlock', () => {
6673
})
6774
})
6875

76+
describe('nullable candidate updates', () => {
77+
it('preserves nulls from dynamic references for aliased update fields', () => {
78+
const result = AshbyBlock.tools.config.params!(
79+
buildParams('update_candidate', {
80+
updateName: null,
81+
candidateLocation: null,
82+
candidateCreatedAt: null,
83+
})
84+
)
85+
expect(result.name).toBeNull()
86+
expect(result.location).toBeNull()
87+
expect(result.createdAt).toBeNull()
88+
})
89+
})
90+
91+
describe('archiveEmail parsing (change_application_stage)', () => {
92+
it('parses the documented archive email object', () => {
93+
const result = AshbyBlock.tools.config.params!(
94+
buildParams('change_application_stage', {
95+
archiveEmail: '{"communicationTemplateId":"template-1","sendAt":"2026-09-02T16:32:00Z"}',
96+
})
97+
)
98+
expect(result.archiveEmail).toEqual({
99+
communicationTemplateId: 'template-1',
100+
sendAt: '2026-09-02T16:32:00Z',
101+
})
102+
})
103+
104+
it('rejects non-object archive email input', () => {
105+
expect(() =>
106+
AshbyBlock.tools.config.params!(
107+
buildParams('change_application_stage', { archiveEmail: 'true' })
108+
)
109+
).toThrow(/expected a JSON object/)
110+
})
111+
})
112+
69113
describe('wandConfig on array-shaped fields', () => {
70114
it('does not request object-wrapped output for alternateEmailAddresses or socialLinks', () => {
71115
// generationType 'json-object' makes the wand API append "the response
@@ -324,6 +368,57 @@ describe('AshbyBlock', () => {
324368
const condition = syncToken?.condition as { value: string[] }
325369
expect(condition.value).toContain('list_jobs')
326370
})
371+
372+
it('maps the editor status selection to the tool array contract', () => {
373+
const result = AshbyBlock.tools.config.params!(
374+
buildParams('list_jobs', { jobStatus: 'Open' })
375+
)
376+
expect(result.status).toEqual(['Open'])
377+
})
378+
379+
it('maps the shared draft-posting switch to the endpoint-specific parameter', () => {
380+
const listParams = AshbyBlock.tools.config.params!(
381+
buildParams('list_jobs', { includeUnpublishedJobPostingIds: true })
382+
)
383+
expect(listParams.includeUnpublishedJobPostingsIds).toBe(true)
384+
expect(listParams.includeUnpublishedJobPostingIds).toBeUndefined()
385+
386+
const infoParams = AshbyBlock.tools.config.params!(
387+
buildParams('get_job', { includeUnpublishedJobPostingIds: true })
388+
)
389+
expect(infoParams.includeUnpublishedJobPostingIds).toBe(true)
390+
expect(infoParams.includeUnpublishedJobPostingsIds).toBeUndefined()
391+
})
392+
})
393+
394+
describe('expanded list controls', () => {
395+
it('exposes only provider-supported pagination and sync controls', () => {
396+
const cursor = AshbyBlock.subBlocks.find((s) => s.id === 'cursor')
397+
const cursorOperations = (cursor?.condition as { value: string[] }).value
398+
expect(cursorOperations).not.toContain('search_candidates')
399+
expect(cursorOperations).not.toContain('search_jobs')
400+
401+
const syncToken = AshbyBlock.subBlocks.find((s) => s.id === 'syncToken')
402+
const syncOperations = (syncToken?.condition as { value: string[] }).value
403+
expect(syncOperations).toContain('list_application_feedback')
404+
})
405+
406+
it('exposes archived interview plans in the editor', () => {
407+
const includeArchived = AshbyBlock.subBlocks.find((s) => s.id === 'includeArchived')
408+
const operations = (includeArchived?.condition as { value: string[] }).value
409+
expect(operations).toContain('list_interview_plans')
410+
})
411+
})
412+
413+
describe('alternate lookup identifiers', () => {
414+
it('does not require the Ashby UUID when an alternate lookup is supported', () => {
415+
const candidateId = AshbyBlock.subBlocks.find((s) => s.id === 'candidateId')
416+
const applicationId = AshbyBlock.subBlocks.find((s) => s.id === 'applicationId')
417+
const candidateRequired = candidateId?.required as { value?: string[] }
418+
const applicationRequired = applicationId?.required as { value?: string[] }
419+
expect(candidateRequired.value).not.toContain('get_candidate')
420+
expect(applicationRequired.value).not.toContain('get_application')
421+
})
327422
})
328423

329424
describe('list_applications candidateId filter', () => {

0 commit comments

Comments
 (0)