diff --git a/apps/sim/blocks/blocks/ashby.test.ts b/apps/sim/blocks/blocks/ashby.test.ts new file mode 100644 index 00000000000..75a26b13b68 --- /dev/null +++ b/apps/sim/blocks/blocks/ashby.test.ts @@ -0,0 +1,85 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { AshbyBlock } from './ashby' + +describe('AshbyBlock', () => { + const buildParams = (operation: string, extra: Record) => ({ + operation, + ...extra, + }) + + describe('alternateEmailAddresses parsing (create_candidate)', () => { + it('parses a comma-separated string into an array', () => { + const result = AshbyBlock.tools.config.params!( + buildParams('create_candidate', { + alternateEmailAddresses: 'a@x.com, b@x.com', + }) + ) + expect(result.alternateEmailAddresses).toEqual(['a@x.com', 'b@x.com']) + }) + + it('parses a JSON array string into an array', () => { + const result = AshbyBlock.tools.config.params!( + buildParams('create_candidate', { + alternateEmailAddresses: '["a@x.com","b@x.com"]', + }) + ) + expect(result.alternateEmailAddresses).toEqual(['a@x.com', 'b@x.com']) + }) + + it('omits the field entirely when empty', () => { + const result = AshbyBlock.tools.config.params!( + buildParams('create_candidate', { alternateEmailAddresses: '' }) + ) + expect(result.alternateEmailAddresses).toBeUndefined() + }) + }) + + describe('socialLinks parsing (update_candidate)', () => { + it('parses a JSON array of link objects', () => { + const result = AshbyBlock.tools.config.params!( + buildParams('update_candidate', { + socialLinks: '[{"type":"Twitter","url":"https://twitter.com/jane"}]', + }) + ) + expect(result.socialLinks).toEqual([{ type: 'Twitter', url: 'https://twitter.com/jane' }]) + }) + + it('omits the field when the JSON is malformed', () => { + const result = AshbyBlock.tools.config.params!( + buildParams('update_candidate', { socialLinks: 'not json' }) + ) + expect(result.socialLinks).toBeUndefined() + }) + }) + + describe('wandConfig on array-shaped fields', () => { + it('does not request object-wrapped output for alternateEmailAddresses or socialLinks', () => { + // generationType 'json-object' makes the wand API append "the response + // must start with { and end with }", which conflicts with these fields' + // array-or-comma-separated parsers (parseStringListInput/parseSocialLinksInput). + const alternateEmailAddresses = AshbyBlock.subBlocks.find( + (s) => s.id === 'alternateEmailAddresses' + ) + const socialLinks = AshbyBlock.subBlocks.find((s) => s.id === 'socialLinks') + expect(alternateEmailAddresses?.wandConfig?.generationType).not.toBe('json-object') + expect(socialLinks?.wandConfig?.generationType).not.toBe('json-object') + }) + }) + + describe('list_applications candidateId filter', () => { + it('has no filterCandidateId subBlock or wiring', () => { + // Ashby's application.list endpoint silently ignores a candidateId + // body field (confirmed live: passing a nonexistent candidate UUID + // still returns unfiltered results) — the correct path for a + // candidate's applications is ashby_get_candidate's applicationIds. + expect(AshbyBlock.subBlocks.find((s) => s.id === 'filterCandidateId')).toBeUndefined() + const result = AshbyBlock.tools.config.params!( + buildParams('list_applications', { filterCandidateId: 'some-id' }) + ) + expect(result.candidateId).toBeUndefined() + }) + }) +}) diff --git a/apps/sim/blocks/blocks/ashby.ts b/apps/sim/blocks/blocks/ashby.ts index 60a4cbe2aad..78c63efee30 100644 --- a/apps/sim/blocks/blocks/ashby.ts +++ b/apps/sim/blocks/blocks/ashby.ts @@ -2,6 +2,34 @@ import { AshbyIcon } from '@/components/icons' import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' import { getTrigger } from '@/triggers' +function parseStringListInput(value: unknown): string[] { + if (Array.isArray(value)) return value.map(String) + if (typeof value !== 'string') return [] + const trimmed = value.trim() + if (!trimmed) return [] + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed) + if (Array.isArray(parsed)) return parsed.map(String) + } catch {} + } + return trimmed + .split(',') + .map((s) => s.trim()) + .filter(Boolean) +} + +function parseSocialLinksInput(value: unknown): Array<{ type: string; url: string }> { + if (Array.isArray(value)) return value as Array<{ type: string; url: string }> + if (typeof value !== 'string' || !value.trim()) return [] + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) ? parsed : [] + } catch { + return [] + } +} + export const AshbyBlock: BlockConfig = { type: 'ashby', name: 'Ashby', @@ -368,14 +396,6 @@ Output only the ISO 8601 timestamp string, nothing else.`, condition: { field: 'operation', value: 'list_applications' }, mode: 'advanced', }, - { - id: 'filterCandidateId', - title: 'Candidate ID Filter', - type: 'short-input', - placeholder: 'Filter by candidate UUID', - condition: { field: 'operation', value: 'list_applications' }, - mode: 'advanced', - }, { id: 'createdAfter', title: 'Created After', @@ -563,6 +583,13 @@ Output only the ISO 8601 timestamp string, nothing else.`, placeholder: 'Comma-separated or JSON array (e.g. ["a@x.com","b@x.com"])', condition: { field: 'operation', value: 'create_candidate' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a comma-separated or JSON array of email addresses based on the user's description. +Examples: +- "her work and personal emails" -> ["work@company.com","personal@example.com"] +Output only the list, nothing else.`, + }, }, { id: 'socialLinks', @@ -571,6 +598,13 @@ Output only the ISO 8601 timestamp string, nothing else.`, placeholder: 'JSON array (e.g. [{"type":"Twitter","url":"https://twitter.com/x"}])', condition: { field: 'operation', value: 'update_candidate' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of social link objects ({"type","url"}) based on the user's description. +Examples: +- "his Twitter is @jane and portfolio is jane.dev" -> [{"type":"Twitter","url":"https://twitter.com/jane"},{"type":"Portfolio","url":"https://jane.dev"}] +Output only the JSON array, nothing else.`, + }, }, { id: 'includeArchived', @@ -720,9 +754,6 @@ Output only the ISO 8601 timestamp string, nothing else.`, if (params.searchEmail) result.email = params.searchEmail if (params.filterStatus) result.status = params.filterStatus if (params.filterJobId) result.jobId = params.filterJobId - if (params.operation === 'list_applications' && params.filterCandidateId) { - result.candidateId = params.filterCandidateId - } if (params.jobStatus) result.status = params.jobStatus if (params.sendNotifications === 'true' || params.sendNotifications === true) { result.sendNotifications = true @@ -772,9 +803,14 @@ Output only the ISO 8601 timestamp string, nothing else.`, result.applicationId = params.offerApplicationId } if (params.alternateEmailAddresses) { - result.alternateEmailAddresses = params.alternateEmailAddresses + const alternateEmailAddresses = parseStringListInput(params.alternateEmailAddresses) + if (alternateEmailAddresses.length > 0) + result.alternateEmailAddresses = alternateEmailAddresses + } + if (params.socialLinks) { + const socialLinks = parseSocialLinksInput(params.socialLinks) + if (socialLinks.length > 0) result.socialLinks = socialLinks } - if (params.socialLinks) result.socialLinks = params.socialLinks return result }, }, @@ -806,7 +842,6 @@ Output only the ISO 8601 timestamp string, nothing else.`, sendNotifications: { type: 'boolean', description: 'Send notifications' }, filterStatus: { type: 'string', description: 'Application status filter' }, filterJobId: { type: 'string', description: 'Job UUID filter' }, - filterCandidateId: { type: 'string', description: 'Candidate UUID filter' }, createdAfter: { type: 'string', description: 'Filter by creation date' }, openedAfter: { type: 'string', description: 'Filter jobs opened after this timestamp' }, openedBefore: { type: 'string', description: 'Filter jobs opened before this timestamp' }, diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.ts index d56d51ff055..a97d73e4e56 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.ts @@ -46,6 +46,7 @@ export const SUBBLOCK_ID_MIGRATIONS: Record> = { phoneType: '_removed_phoneType', expandApplicationFormDefinition: '_removed_expandApplicationFormDefinition', expandSurveyFormDefinitions: '_removed_expandSurveyFormDefinitions', + filterCandidateId: '_removed_filterCandidateId', }, apollo: { contact_ids_bulk: 'contacts', diff --git a/apps/sim/tools/ashby/list_applications.ts b/apps/sim/tools/ashby/list_applications.ts index e347f083612..00201c9698d 100644 --- a/apps/sim/tools/ashby/list_applications.ts +++ b/apps/sim/tools/ashby/list_applications.ts @@ -51,12 +51,6 @@ export const listApplicationsTool: ToolConfig< visibility: 'user-or-llm', description: 'Filter applications by a specific job UUID', }, - candidateId: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Filter applications by a specific candidate UUID', - }, createdAfter: { type: 'string', required: false, @@ -76,7 +70,6 @@ export const listApplicationsTool: ToolConfig< if (params.perPage) body.limit = params.perPage if (params.status) body.status = params.status if (params.jobId) body.jobId = params.jobId.trim() - if (params.candidateId) body.candidateId = params.candidateId.trim() if (params.createdAfter) { const ms = new Date(params.createdAfter).getTime() if (!Number.isNaN(ms)) body.createdAfter = ms diff --git a/apps/sim/tools/ashby/types.ts b/apps/sim/tools/ashby/types.ts index 4dc5e03892b..800728b5948 100644 --- a/apps/sim/tools/ashby/types.ts +++ b/apps/sim/tools/ashby/types.ts @@ -148,7 +148,6 @@ export interface AshbyListApplicationsParams extends AshbyBaseParams { perPage?: number status?: string jobId?: string - candidateId?: string createdAfter?: string }