Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions apps/sim/blocks/blocks/ashby.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) => ({
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()
})
})
})
63 changes: 49 additions & 14 deletions apps/sim/blocks/blocks/ashby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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.`,
},
Comment thread
waleedlatif1 marked this conversation as resolved.
},
{
id: 'socialLinks',
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
},
},
Expand Down Expand Up @@ -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' },
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/workflows/migrations/subblock-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const SUBBLOCK_ID_MIGRATIONS: Record<string, Record<string, string>> = {
phoneType: '_removed_phoneType',
expandApplicationFormDefinition: '_removed_expandApplicationFormDefinition',
expandSurveyFormDefinitions: '_removed_expandSurveyFormDefinitions',
filterCandidateId: '_removed_filterCandidateId',
},
apollo: {
contact_ids_bulk: 'contacts',
Expand Down
7 changes: 0 additions & 7 deletions apps/sim/tools/ashby/list_applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion apps/sim/tools/ashby/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ export interface AshbyListApplicationsParams extends AshbyBaseParams {
perPage?: number
status?: string
jobId?: string
candidateId?: string
createdAfter?: string
}

Expand Down
Loading