From c5cac36ed12d82e74bd7ff3ae9009bf321e6cdb8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 16:52:08 -0700 Subject: [PATCH 01/11] fix(greenhouse): reproduce configured agent input omissions --- .../migrations/subblock-migrations.test.ts | 50 +++++++++++ apps/sim/providers/utils.test.ts | 88 +++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts index 0f498d913ee..5499830e9dd 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts @@ -126,6 +126,56 @@ describe('migration targets', () => { }) describe('migrateSubblockIds', () => { + it.each(['42', ''])( + 'preserves the active Greenhouse alias %j over an unrelated stored target', + (value) => { + const block = makeBlock({ + type: 'greenhouse_v2', + subBlocks: { + operation: { id: 'operation', type: 'dropdown', value: 'convert_prospect' }, + id: { id: 'id', type: 'short-input', value: 'stale-candidate' }, + update_application_id: { id: 'update_application_id', type: 'short-input', value }, + job_id: { id: 'job_id', type: 'short-input', value: '123' }, + convert_prospect_job_id: { + id: 'convert_prospect_job_id', + type: 'short-input', + value: '', + }, + created_at_filter: { + id: 'created_at_filter', + type: 'code', + value: '{"gte":"2026-01-01"}', + }, + }, + }) + const result = migrateSubblockIds({ 'block-1': block }) + const saved = result.blocks['block-1'].subBlocks + expect(saved.id.value).toBe(value) + expect(saved.job_id.value).toBe('') + expect(saved).not.toHaveProperty('update_application_id') + expect(saved).not.toHaveProperty('convert_prospect_job_id') + expect(saved.created_at_filter).toEqual(block.subBlocks.created_at_filter) + expect(block.subBlocks.id.value).toBe('stale-candidate') + expect(migrateSubblockIds(result.blocks).migrated).toBe(false) + } + ) + + it('leaves canonical Greenhouse fields and legacy blocks unchanged without an active retired alias', () => { + for (const type of ['greenhouse_v2', 'greenhouse']) { + const block = makeBlock({ + type, + subBlocks: { + operation: { id: 'operation', type: 'dropdown', value: 'get_candidate' }, + id: { id: 'id', type: 'short-input', value: '42' }, + update_application_id: { id: 'update_application_id', type: 'short-input', value: '99' }, + }, + }) + const result = migrateSubblockIds({ 'block-1': block }) + expect(result.migrated).toBe(false) + expect(result.blocks['block-1']).toBe(block) + } + }) + it('preserves MCP canonical modes through the full normalization pipeline', () => { const block = makeBlock({ type: 'mcp', diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index d00d45ddf1f..22a61feb270 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -11,6 +11,7 @@ vi.mock('@/lib/internal/workflows/read-tool-enrichment', () => ({ readWorkflowMetadataForTool: workflowMetadataMocks.readWorkflowMetadataForTool, })) +import { GreenhouseV2Block } from '@/blocks/blocks/greenhouse' import { IntuneBlock } from '@/blocks/blocks/intune' import { MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel' import { RampBlock } from '@/blocks/blocks/ramp' @@ -43,11 +44,19 @@ import { transformBlockTool, } from '@/providers/utils' import { useProvidersStore } from '@/stores/providers/store' +import { greenhouseConvertProspectV2Tool } from '@/tools/greenhouse/convert_prospect_v2' +import { greenhouseListCandidatesV2Tool } from '@/tools/greenhouse/list_candidates_v2' +import type { + GreenhouseConvertProspectV2Params, + GreenhouseListCandidatesV2Params, +} from '@/tools/greenhouse/types' import { intuneListDetectedAppsTool } from '@/tools/intune/list-detected-apps' import { intuneListDevicesTool } from '@/tools/intune/list-devices' import { clearRangeTool } from '@/tools/microsoft_excel/clear_range' import { createTableTool } from '@/tools/microsoft_excel/create_table' import { sortRangeTool } from '@/tools/microsoft_excel/sort_range' +import * as toolMetadata from '@/tools/metadata' +import { getSubBlocksForToolInput } from '@/tools/params' import { rampListReimbursementsTool } from '@/tools/ramp/list-reimbursements' import { rampListTransactionsTool } from '@/tools/ramp/list-transactions' import { revenuecatGetCustomerTool } from '@/tools/revenuecat/get_customer' @@ -58,6 +67,85 @@ const mockGetRotatingApiKey = vi.fn().mockReturnValue('rotating-server-key') const originalRequire = module.require describe('integration filters through agent execution', () => { + it('sends configured Greenhouse conversion fields from the offered controls to the API', async () => { + const tool = greenhouseConvertProspectV2Tool + vi.spyOn(toolMetadata, 'getToolMetadata').mockReturnValue(tool) + const fields = getSubBlocksForToolInput( + tool.id, + 'greenhouse_v2', + { operation: 'convert_prospect' }, + undefined, + GreenhouseV2Block + )?.subBlocks + const idField = fields?.find((field) => field.id === 'id') + const jobField = fields?.find((field) => field.id === 'job_id') + if (!idField || !jobField) throw new Error('Conversion target controls are unavailable') + const providerTool = await transformBlockTool( + { type: 'greenhouse_v2', params: { [idField.id]: '42', [jobField.id]: '81' } }, + { + selectedOperation: 'convert_prospect', + getAllBlocks: () => [GreenhouseV2Block], + getTool: () => tool, + } + ) + if (!providerTool) throw new Error('Greenhouse agent tool was not created') + const { executionParams } = prepareToolExecution( + providerTool, + { id: 99, job_id: 98 }, + {}, + undefined + ) + const params = { + ...executionParams, + accessToken: 'fixture-token', + } as GreenhouseConvertProspectV2Params + const url = typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url + expect(new URL(url).pathname).toBe('/v3/applications/42/convert_to_candidate') + expect(tool.request.body?.(params)).toEqual({ job_id: 81 }) + }) + + it.each(['configured', 'model'] as const)( + 'sends a %s Greenhouse date range without a stored operation', + async (source) => { + const tool = greenhouseListCandidatesV2Tool + vi.spyOn(toolMetadata, 'getToolMetadata').mockReturnValue(tool) + const field = getSubBlocksForToolInput( + tool.id, + 'greenhouse_v2', + { operation: 'list_candidates' }, + undefined, + GreenhouseV2Block + )?.subBlocks.find((item) => item.id === 'created_at') + if (!field) throw new Error('Created-at filter control is unavailable') + const range = { gte: '2026-01-01T00:00:00Z' } + const providerTool = await transformBlockTool( + { + type: 'greenhouse_v2', + params: source === 'configured' ? { [field.id]: JSON.stringify(range) } : {}, + }, + { + selectedOperation: 'list_candidates', + getAllBlocks: () => [GreenhouseV2Block], + getTool: () => tool, + } + ) + if (!providerTool) throw new Error('Greenhouse agent tool was not created') + const { executionParams } = prepareToolExecution( + providerTool, + { created_at: range }, + {}, + undefined + ) + const params = { + ...executionParams, + accessToken: 'fixture-token', + } as GreenhouseListCandidatesV2Params + const url = + typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url + expect(new URL(url).searchParams.get('created_at')).toBe('gte|2026-01-01T00:00:00Z') + } + ) + const rampCases = [ { operation: 'list_transactions', From 8286124279b43f8fed444b959acc8151b8927c73 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 16:59:07 -0700 Subject: [PATCH 02/11] fix(greenhouse): align configured agent inputs with API parameters --- .../content/docs/integrations/greenhouse.mdx | 2 + apps/sim/blocks/blocks/greenhouse.ts | 652 ++++++++---------- .../migrations/greenhouse-agent-inputs.ts | 75 ++ .../migrations/subblock-migrations.test.ts | 50 +- .../migrations/subblock-migrations.ts | 8 +- apps/sim/serializer/index.ts | 5 +- 6 files changed, 409 insertions(+), 383 deletions(-) create mode 100644 apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts diff --git a/apps/docs/content/docs/integrations/greenhouse.mdx b/apps/docs/content/docs/integrations/greenhouse.mdx index b72085f4def..93d259a2f84 100644 --- a/apps/docs/content/docs/integrations/greenhouse.mdx +++ b/apps/docs/content/docs/integrations/greenhouse.mdx @@ -27,6 +27,8 @@ Harvest v3 returns applications, attachments, education, and employment through List operations return one page, its `count`, `nextCursor`, and `hasMore`. Set a page size from 1 to 500 and pass `nextCursor` into the next call until `hasMore` is false. A cursor carries the initial query, so other filters and page size are ignored while it is set. Date filters accept a JSON object such as `{"gte":"2026-01-01T00:00:00Z"}`; use either `created_at` or `updated_at`, not both. +For list operations, Job IDs uses comma-separated IDs. For prospect application creation, Job IDs uses a JSON array of integers. Created At takes a JSON date range on list operations, an ISO 8601 timestamp when creating an application, and a `YYYY-MM-DD` date when updating an application. + Partial reads leave unrequested fields as `null`, including collections. An explicit empty array means the collection is empty. Date-only filters, such as offer `sent_on` and `starts_on`, use `YYYY-MM-DD` bounds. When updating candidate contact arrays, tags, or linked users, read the current candidate first and submit the complete desired array: supplied arrays replace their existing values. Unset inputs are omitted; custom-field entries may use `null` to clear a value. Creating an email note records an email in Greenhouse and does not send one. Application movement may trigger Greenhouse stage automation, and rejection can optionally send a rejection email. Move, hire, reject, and unreject operations return no provider fields on success. diff --git a/apps/sim/blocks/blocks/greenhouse.ts b/apps/sim/blocks/blocks/greenhouse.ts index 33b64b28dd6..c159f350493 100644 --- a/apps/sim/blocks/blocks/greenhouse.ts +++ b/apps/sim/blocks/blocks/greenhouse.ts @@ -547,48 +547,6 @@ export const GreenhouseBlockMeta = { ], } as const satisfies BlockMeta -const GREENHOUSE_V2_PARAM_REMAPS: Record> = { - list_application_stages: { created_at: 'created_at_filter' }, - list_applications: { created_at: 'created_at_filter', status: 'applications_status' }, - list_applied_candidate_tags: { created_at: 'created_at_filter' }, - list_attachments: { created_at: 'created_at_filter', type: 'attachments_type' }, - list_candidate_educations: { created_at: 'created_at_filter' }, - list_candidate_employments: { created_at: 'created_at_filter' }, - list_candidate_tags: { created_at: 'created_at_filter' }, - list_candidates: { created_at: 'created_at_filter' }, - list_close_reasons: { created_at: 'created_at_filter' }, - list_custom_field_options: { created_at: 'created_at_filter' }, - list_custom_fields: { created_at: 'created_at_filter' }, - list_departments: { created_at: 'created_at_filter' }, - list_email_templates: { created_at: 'created_at_filter' }, - list_interviews: { created_at: 'created_at_filter', status: 'interviews_status' }, - list_job_hiring_managers: { created_at: 'created_at_filter' }, - list_job_interview_stages: { created_at: 'created_at_filter' }, - list_job_owners: { created_at: 'created_at_filter', type: 'job_owners_type' }, - list_jobs: { created_at: 'created_at_filter', status: 'jobs_status' }, - list_notes: { created_at: 'created_at_filter', visibility: 'notes_visibility' }, - list_offers: { created_at: 'created_at_filter', status: 'offers_status' }, - list_offices: { created_at: 'created_at_filter' }, - list_openings: { created_at: 'created_at_filter' }, - list_prospect_pool_stages: { created_at: 'created_at_filter' }, - list_prospect_pools: { created_at: 'created_at_filter' }, - list_referrers: { created_at: 'created_at_filter' }, - list_rejection_reasons: { created_at: 'created_at_filter' }, - list_scorecards: { created_at: 'created_at_filter', status: 'scorecards_status' }, - list_sources: { created_at: 'created_at_filter' }, - list_users: { created_at: 'created_at_filter' }, - create_attachment: { type: 'attachments_type', visibility: 'attachment_visibility' }, - update_application: { id: 'update_application_id', created_at: 'application_created_on' }, - update_candidate: { id: 'update_application_id' }, - convert_prospect: { id: 'update_application_id', job_id: 'convert_prospect_job_id' }, - hire_application: { id: 'update_application_id', opening_id: 'hire_application_opening_id' }, - move_application: { id: 'update_application_id' }, - reject_application: { id: 'update_application_id' }, - unreject_application: { id: 'update_application_id' }, - create_application: { job_ids: 'create_application_job_ids' }, - create_note: { visibility: 'attachment_visibility' }, -} - export const GreenhouseV2Block: BlockConfig = { type: 'greenhouse_v2', name: 'Greenhouse', @@ -656,28 +614,14 @@ export const GreenhouseV2Block: BlockConfig = { list_sources: ['List sources'], list_users: ['List users'], get_user: [{ text: 'Get user', field: 'id', core: true }], - update_application: [ - { text: 'Update application', field: 'update_application_id', core: true }, - ], - update_candidate: [ - { text: 'Update candidate', field: 'update_application_id', core: true }, - ], + update_application: [{ text: 'Update application', field: 'id', core: true }], + update_candidate: [{ text: 'Update candidate', field: 'id', core: true }], create_application: ['Create application'], - convert_prospect: [ - { text: 'Convert prospect', field: 'update_application_id', core: true }, - ], - hire_application: [ - { text: 'Hire application', field: 'update_application_id', core: true }, - ], - move_application: [ - { text: 'Move application', field: 'update_application_id', core: true }, - ], - reject_application: [ - { text: 'Reject application', field: 'update_application_id', core: true }, - ], - unreject_application: [ - { text: 'Unreject application', field: 'update_application_id', core: true }, - ], + convert_prospect: [{ text: 'Convert prospect', field: 'id', core: true }], + hire_application: [{ text: 'Hire application', field: 'id', core: true }], + move_application: [{ text: 'Move application', field: 'id', core: true }], + reject_application: [{ text: 'Reject application', field: 'id', core: true }], + unreject_application: [{ text: 'Unreject application', field: 'id', core: true }], apply_candidate_tag: ['Apply candidate tag'], create_attachment: ['Create attachment'], create_candidate_tag: ['Create candidate tag'], @@ -771,6 +715,13 @@ export const GreenhouseV2Block: BlockConfig = { 'get_candidate', 'get_job', 'get_user', + 'update_application', + 'update_candidate', + 'convert_prospect', + 'hire_application', + 'move_application', + 'reject_application', + 'unreject_application', ], }, required: { @@ -784,6 +735,13 @@ export const GreenhouseV2Block: BlockConfig = { 'get_candidate', 'get_job', 'get_user', + 'update_application', + 'update_candidate', + 'convert_prospect', + 'hire_application', + 'move_application', + 'reject_application', + 'unreject_application', ], }, }, @@ -919,13 +877,14 @@ export const GreenhouseV2Block: BlockConfig = { mode: 'advanced', }, { - id: 'created_at_filter', - title: 'Created At Filter', - type: 'code', - placeholder: '{"gte":"2026-01-01T00:00:00Z"}', + id: 'created_at', + title: 'Created At', + type: 'long-input', + placeholder: 'Date, timestamp, or JSON range for the selected operation', condition: { field: 'operation', value: [ + 'create_application', 'list_application_stages', 'list_applications', 'list_applied_candidate_tags', @@ -955,17 +914,20 @@ export const GreenhouseV2Block: BlockConfig = { 'list_scorecards', 'list_sources', 'list_users', + 'update_application', ], }, required: false, - language: 'json', wandConfig: { enabled: true, + generationType: 'timestamp', prompt: - 'Created at filter as JSON with gte, lte, gt, lt keys and ISO 8601 values; cannot combine with updated_at, for example {"gte":"2026-01-01T00:00:00Z"}. Return ONLY the JSON.', - placeholder: 'Describe the created at', + 'For list operations, return ONLY a JSON object with gte/lte/gt/lt keys and ISO 8601 timestamps. For Create Application, return ONLY an ISO 8601 timestamp. For Update Application, return ONLY a YYYY-MM-DD date. Use the requested operation.', + placeholder: 'Describe the date range or date and operation', }, mode: 'advanced', + description: + 'Lists: JSON range using gte, lte, gt, lt with ISO 8601 timestamps; cannot combine with Updated At. Create Application: ISO 8601 timestamp. Update Application: YYYY-MM-DD date.', }, { id: 'updated_at', @@ -1147,8 +1109,8 @@ export const GreenhouseV2Block: BlockConfig = { { id: 'job_ids', title: 'Job IDs', - type: 'short-input', - placeholder: '123,456', + type: 'long-input', + placeholder: 'List filter: 123,456; prospect creation: [123,456]', condition: { field: 'operation', value: [ @@ -1160,16 +1122,19 @@ export const GreenhouseV2Block: BlockConfig = { 'list_offers', 'list_openings', 'list_prospect_pools', + 'create_application', ], }, required: false, wandConfig: { enabled: true, prompt: - 'Job IDs Comma-separated values (maximum 50). Return ONLY the comma-separated list.', - placeholder: 'Describe the values to include', + 'For list operations, return ONLY comma-separated job IDs (maximum 50). For Create Application with Prospect enabled, return ONLY a JSON array of integer job IDs.', + placeholder: 'Describe the job IDs and operation', }, mode: 'advanced', + description: + 'For list operations, comma-separated IDs (maximum 50). For prospect application creation, a JSON array of integer job IDs.', }, { id: 'prospective_job_ids', @@ -1247,19 +1212,88 @@ export const GreenhouseV2Block: BlockConfig = { mode: 'advanced', }, { - id: 'applications_status', + id: 'status', title: 'Status', type: 'dropdown', - condition: { field: 'operation', value: ['list_applications'] }, + condition: { + field: 'operation', + value: [ + 'list_applications', + 'list_interviews', + 'list_jobs', + 'list_offers', + 'list_scorecards', + ], + }, required: false, - options: [ - { label: 'Not set', id: '' }, - { label: 'Rejected', id: 'rejected' }, - { label: 'Hired', id: 'hired' }, - { label: 'Converted', id: 'converted' }, - { label: 'Active', id: 'active' }, - ], - mode: 'advanced', + options: (params) => { + const options: Record = { + list_applications: [ + { label: 'Not set', id: '' }, + { label: 'Rejected', id: 'rejected' }, + { label: 'Hired', id: 'hired' }, + { label: 'Converted', id: 'converted' }, + { label: 'Active', id: 'active' }, + ], + list_interviews: [ + { label: 'Not set', id: '' }, + { label: 'To Be Scheduled', id: 'to_be_scheduled' }, + { label: 'Scheduled', id: 'scheduled' }, + { label: 'Awaiting Feedback', id: 'awaiting_feedback' }, + { label: 'Complete', id: 'complete' }, + { label: 'Skipped', id: 'skipped' }, + { label: 'Collect Feedback', id: 'collect_feedback' }, + { label: 'To Be Sent', id: 'to_be_sent' }, + { label: 'Sent', id: 'sent' }, + { label: 'Received', id: 'received' }, + ], + list_jobs: [ + { label: 'Not set', id: '' }, + { label: 'Open', id: 'open' }, + { label: 'Draft', id: 'draft' }, + { label: 'Closed', id: 'closed' }, + ], + list_offers: [ + { label: 'Not set', id: '' }, + { label: 'Created', id: 'Created' }, + { label: 'Accepted', id: 'Accepted' }, + { label: 'Rejected', id: 'Rejected' }, + { label: 'Deprecated', id: 'Deprecated' }, + ], + list_scorecards: [ + { label: 'Not set', id: '' }, + { label: 'Draft', id: 'draft' }, + { label: 'Complete', id: 'complete' }, + ], + } + return ( + options[String(params?.values.operation)] ?? [ + { label: 'Not set', id: '' }, + { label: 'Rejected', id: 'rejected' }, + { label: 'Hired', id: 'hired' }, + { label: 'Converted', id: 'converted' }, + { label: 'Active', id: 'active' }, + { label: 'To Be Scheduled', id: 'to_be_scheduled' }, + { label: 'Scheduled', id: 'scheduled' }, + { label: 'Awaiting Feedback', id: 'awaiting_feedback' }, + { label: 'Complete', id: 'complete' }, + { label: 'Skipped', id: 'skipped' }, + { label: 'Collect Feedback', id: 'collect_feedback' }, + { label: 'To Be Sent', id: 'to_be_sent' }, + { label: 'Sent', id: 'sent' }, + { label: 'Received', id: 'received' }, + { label: 'Open', id: 'open' }, + { label: 'Draft', id: 'draft' }, + { label: 'Closed', id: 'closed' }, + { label: 'Created', id: 'Created' }, + { label: 'Accepted', id: 'Accepted' }, + { label: 'Rejected', id: 'Rejected' }, + { label: 'Deprecated', id: 'Deprecated' }, + ] + ) + }, + mode: 'advanced', + emptyIsValid: true, }, { id: 'stage_name', @@ -1333,23 +1367,90 @@ export const GreenhouseV2Block: BlockConfig = { mode: 'advanced', }, { - id: 'attachments_type', + id: 'type', title: 'Type', - type: 'dropdown', - condition: { field: 'operation', value: ['list_attachments', 'create_attachment'] }, - required: { field: 'operation', value: ['create_attachment'] }, - options: [ - { label: 'Resume', id: 'resume' }, - { label: 'Cover Letter', id: 'cover_letter' }, - { label: 'Take Home Test', id: 'take_home_test' }, - { label: 'Offer Packet', id: 'offer_packet' }, - { label: 'Offer Letter', id: 'offer_letter' }, - { label: 'Signed Offer Letter', id: 'signed_offer_letter' }, - { label: 'Other', id: 'other' }, - { label: 'Form Attachment', id: 'form_attachment' }, - { label: 'Midfunnel Agreement', id: 'midfunnel_agreement' }, - { label: 'Automated Agreement', id: 'automated_agreement' }, - ], + type: 'combobox', + placeholder: 'Select a type, or enter comma-separated note types', + condition: { + field: 'operation', + value: ['list_notes', 'list_attachments', 'create_attachment', 'list_job_owners'], + }, + required: { + field: 'operation', + value: 'create_attachment', + }, + options: (params) => { + const options: Record = { + list_notes: [ + { label: 'Not set', id: '' }, + { label: 'Note', id: 'NOTE' }, + { label: 'Activity', id: 'ACTIVITY' }, + { label: 'Interview', id: 'INTERVIEW' }, + { label: 'Email', id: 'EMAIL' }, + { label: 'Follow Up', id: 'FOLLOW_UP' }, + { label: 'Take Home Test', id: 'TAKE_HOME_TEST' }, + { label: 'Linkedin Note', id: 'LINKEDIN_NOTE' }, + { label: 'Linkedin Inmail', id: 'LINKEDIN_INMAIL' }, + { label: 'Availability Request', id: 'AVAILABILITY_REQUEST' }, + { label: 'Migration Error', id: 'MIGRATION_ERROR' }, + { label: 'Touchpoint', id: 'TOUCHPOINT' }, + { label: 'Form', id: 'FORM' }, + { label: 'Feedback', id: 'FEEDBACK' }, + ], + list_attachments: [ + { label: 'Resume', id: 'resume' }, + { label: 'Cover Letter', id: 'cover_letter' }, + { label: 'Take Home Test', id: 'take_home_test' }, + { label: 'Offer Packet', id: 'offer_packet' }, + { label: 'Offer Letter', id: 'offer_letter' }, + { label: 'Signed Offer Letter', id: 'signed_offer_letter' }, + { label: 'Other', id: 'other' }, + { label: 'Form Attachment', id: 'form_attachment' }, + { label: 'Midfunnel Agreement', id: 'midfunnel_agreement' }, + { label: 'Automated Agreement', id: 'automated_agreement' }, + ], + create_attachment: [ + { label: 'Resume', id: 'resume' }, + { label: 'Cover Letter', id: 'cover_letter' }, + { label: 'Take Home Test', id: 'take_home_test' }, + { label: 'Offer Packet', id: 'offer_packet' }, + { label: 'Offer Letter', id: 'offer_letter' }, + { label: 'Signed Offer Letter', id: 'signed_offer_letter' }, + { label: 'Other', id: 'other' }, + { label: 'Form Attachment', id: 'form_attachment' }, + { label: 'Midfunnel Agreement', id: 'midfunnel_agreement' }, + { label: 'Automated Agreement', id: 'automated_agreement' }, + ], + list_job_owners: [ + { label: 'Not set', id: '' }, + { label: 'Sourcer', id: 'sourcer' }, + { label: 'Recruiter', id: 'recruiter' }, + { label: 'Coordinator', id: 'coordinator' }, + ], + } + return ( + options[String(params?.values.operation)] ?? [ + ...options.list_notes, + { label: 'Resume', id: 'resume' }, + { label: 'Cover Letter', id: 'cover_letter' }, + { label: 'Take Home Test', id: 'take_home_test' }, + { label: 'Offer Packet', id: 'offer_packet' }, + { label: 'Offer Letter', id: 'offer_letter' }, + { label: 'Signed Offer Letter', id: 'signed_offer_letter' }, + { label: 'Other', id: 'other' }, + { label: 'Form Attachment', id: 'form_attachment' }, + { label: 'Midfunnel Agreement', id: 'midfunnel_agreement' }, + { label: 'Automated Agreement', id: 'automated_agreement' }, + { label: 'Not set', id: '' }, + { label: 'Sourcer', id: 'sourcer' }, + { label: 'Recruiter', id: 'recruiter' }, + { label: 'Coordinator', id: 'coordinator' }, + ] + ) + }, + emptyIsValid: true, + description: + 'Attachment type for attachment operations; recruiter, sourcer, or coordinator for job owners; comma-separated note types for List Notes.', }, { id: 'start_at', @@ -1719,26 +1820,6 @@ export const GreenhouseV2Block: BlockConfig = { required: false, mode: 'advanced', }, - { - id: 'interviews_status', - title: 'Status', - type: 'dropdown', - condition: { field: 'operation', value: ['list_interviews'] }, - required: false, - options: [ - { label: 'Not set', id: '' }, - { label: 'To Be Scheduled', id: 'to_be_scheduled' }, - { label: 'Scheduled', id: 'scheduled' }, - { label: 'Awaiting Feedback', id: 'awaiting_feedback' }, - { label: 'Complete', id: 'complete' }, - { label: 'Skipped', id: 'skipped' }, - { label: 'Collect Feedback', id: 'collect_feedback' }, - { label: 'To Be Sent', id: 'to_be_sent' }, - { label: 'Sent', id: 'sent' }, - { label: 'Received', id: 'received' }, - ], - mode: 'advanced', - }, { id: 'user_ids', title: 'User IDs', @@ -1757,20 +1838,6 @@ export const GreenhouseV2Block: BlockConfig = { }, mode: 'advanced', }, - { - id: 'job_owners_type', - title: 'Type', - type: 'dropdown', - condition: { field: 'operation', value: ['list_job_owners'] }, - required: false, - options: [ - { label: 'Not set', id: '' }, - { label: 'Sourcer', id: 'sourcer' }, - { label: 'Recruiter', id: 'recruiter' }, - { label: 'Coordinator', id: 'coordinator' }, - ], - mode: 'advanced', - }, { id: 'opened_at', title: 'Opened At', @@ -1844,46 +1911,51 @@ export const GreenhouseV2Block: BlockConfig = { mode: 'advanced', }, { - id: 'jobs_status', - title: 'Status', - type: 'dropdown', - condition: { field: 'operation', value: ['list_jobs'] }, - required: false, - options: [ - { label: 'Not set', id: '' }, - { label: 'Open', id: 'open' }, - { label: 'Draft', id: 'draft' }, - { label: 'Closed', id: 'closed' }, - ], - mode: 'advanced', - }, - { - id: 'type', - title: 'Type', - type: 'short-input', - placeholder: '123,456', - condition: { field: 'operation', value: ['list_notes'] }, - required: false, - wandConfig: { - enabled: true, - prompt: 'Type Comma-separated values (maximum 50). Return ONLY the comma-separated list.', - placeholder: 'Describe the values to include', - }, - mode: 'advanced', - }, - { - id: 'notes_visibility', + id: 'visibility', title: 'Visibility', type: 'dropdown', - condition: { field: 'operation', value: ['list_notes'] }, - required: false, - options: [ - { label: 'Not set', id: '' }, - { label: 'Admin Only Visible', id: 'admin_only_visible' }, - { label: 'Privately Visible', id: 'privately_visible' }, - { label: 'Publicly Visible', id: 'publicly_visible' }, - ], - mode: 'advanced', + condition: { + field: 'operation', + value: ['list_notes', 'create_attachment', 'create_note'], + }, + required: { + field: 'operation', + value: 'create_note', + }, + options: (params) => { + const options: Record = { + list_notes: [ + { label: 'Not set', id: '' }, + { label: 'Admin Only Visible', id: 'admin_only_visible' }, + { label: 'Privately Visible', id: 'privately_visible' }, + { label: 'Publicly Visible', id: 'publicly_visible' }, + ], + create_attachment: [ + { label: 'Admin Only', id: 'admin_only' }, + { label: 'Private', id: 'private' }, + { label: 'Public', id: 'public' }, + ], + create_note: [ + { label: 'Admin Only', id: 'admin_only' }, + { label: 'Private', id: 'private' }, + { label: 'Public', id: 'public' }, + ], + } + return ( + options[String(params?.values.operation)] ?? [ + { label: 'Not set', id: '' }, + { label: 'Admin Only Visible', id: 'admin_only_visible' }, + { label: 'Privately Visible', id: 'privately_visible' }, + { label: 'Publicly Visible', id: 'publicly_visible' }, + { label: 'Admin Only', id: 'admin_only' }, + { label: 'Private', id: 'private' }, + { label: 'Public', id: 'public' }, + ] + ) + }, + emptyIsValid: false, + description: + 'List Notes uses admin_only_visible, privately_visible, or publicly_visible. Create Note and Create Attachment use admin_only, private, or public.', }, { id: 'opening_ids', @@ -1913,21 +1985,6 @@ export const GreenhouseV2Block: BlockConfig = { ], mode: 'advanced', }, - { - id: 'offers_status', - title: 'Status', - type: 'dropdown', - condition: { field: 'operation', value: ['list_offers'] }, - required: false, - options: [ - { label: 'Not set', id: '' }, - { label: 'Created', id: 'Created' }, - { label: 'Accepted', id: 'Accepted' }, - { label: 'Rejected', id: 'Rejected' }, - { label: 'Deprecated', id: 'Deprecated' }, - ], - mode: 'advanced', - }, { id: 'resolved_at', title: 'Resolved At', @@ -2009,9 +2066,14 @@ export const GreenhouseV2Block: BlockConfig = { title: 'Opening ID', type: 'short-input', placeholder: 'Enter opening id', - condition: { field: 'operation', value: ['list_openings'] }, + condition: { + field: 'operation', + value: ['list_openings', 'hire_application'], + }, required: false, mode: 'advanced', + description: + 'List Openings: external opening ID. Hire Application: numeric internal opening ID returned by List Openings.', }, { id: 'prospect_pool_ids', @@ -2148,19 +2210,6 @@ export const GreenhouseV2Block: BlockConfig = { }, mode: 'advanced', }, - { - id: 'scorecards_status', - title: 'Status', - type: 'dropdown', - condition: { field: 'operation', value: ['list_scorecards'] }, - required: false, - options: [ - { label: 'Not set', id: '' }, - { label: 'Draft', id: 'draft' }, - { label: 'Complete', id: 'complete' }, - ], - mode: 'advanced', - }, { id: 'agency_ids', title: 'Agency IDs', @@ -2274,36 +2323,6 @@ export const GreenhouseV2Block: BlockConfig = { ], mode: 'advanced', }, - { - id: 'update_application_id', - title: 'Resource ID', - type: 'short-input', - placeholder: 'Enter numeric id', - condition: { - field: 'operation', - value: [ - 'update_application', - 'update_candidate', - 'convert_prospect', - 'hire_application', - 'move_application', - 'reject_application', - 'unreject_application', - ], - }, - required: { - field: 'operation', - value: [ - 'update_application', - 'update_candidate', - 'convert_prospect', - 'hire_application', - 'move_application', - 'reject_application', - 'unreject_application', - ], - }, - }, { id: 'source_id', title: 'Source ID', @@ -2373,36 +2392,6 @@ export const GreenhouseV2Block: BlockConfig = { }, mode: 'advanced', }, - { - id: 'application_created_on', - title: 'Application Creation Date', - type: 'short-input', - placeholder: '2026-01-01', - condition: { field: 'operation', value: ['update_application'] }, - required: false, - wandConfig: { - enabled: true, - generationType: 'timestamp', - prompt: 'Generate an ISO 8601 date (YYYY-MM-DD). Return ONLY the formatted date.', - placeholder: 'Describe the date', - }, - mode: 'advanced', - }, - { - id: 'created_at', - title: 'Created At', - type: 'short-input', - placeholder: '2026-01-01T00:00:00Z', - condition: { field: 'operation', value: ['create_application'] }, - required: false, - wandConfig: { - enabled: true, - generationType: 'timestamp', - prompt: 'Generate an ISO 8601 timestamp. Return ONLY the formatted timestamp.', - placeholder: 'Describe the date', - }, - mode: 'advanced', - }, { id: 'custom_fields', title: 'Custom Fields', @@ -2791,20 +2780,20 @@ export const GreenhouseV2Block: BlockConfig = { title: 'Job ID', type: 'short-input', placeholder: 'Enter numeric job ID', - condition: { field: 'operation', value: 'create_application' }, - required: { + condition: { field: 'operation', - value: 'create_application', - and: { field: 'prospect', value: ['true', true], not: true }, + value: ['create_application', 'convert_prospect'], }, - }, - { - id: 'convert_prospect_job_id', - title: 'Job ID', - type: 'short-input', - placeholder: 'Enter numeric job ID', - condition: { field: 'operation', value: 'convert_prospect' }, - required: true, + required: (values) => ({ + field: 'operation', + value: + values?.operation === 'convert_prospect' || + (values !== undefined && values.prospect !== true && values.prospect !== 'true') + ? ['create_application', 'convert_prospect'] + : ['convert_prospect'], + }), + description: + 'For application creation, required unless Prospect is true. For conversion, select a job already associated with the prospect or its pool.', }, { id: 'initial_stage_id', @@ -2815,21 +2804,6 @@ export const GreenhouseV2Block: BlockConfig = { required: false, mode: 'advanced', }, - { - id: 'create_application_job_ids', - title: 'Job IDs', - type: 'code', - placeholder: '[]', - condition: { field: 'operation', value: ['create_application'] }, - required: false, - language: 'json', - wandConfig: { - enabled: true, - prompt: 'Prospect job IDs as a JSON array of integers. Return ONLY the JSON.', - placeholder: 'Describe the job ids', - }, - mode: 'advanced', - }, { id: 'prospect_pool_stage_id', title: 'Prospect Pool Stage ID', @@ -2890,15 +2864,6 @@ export const GreenhouseV2Block: BlockConfig = { }, mode: 'advanced', }, - { - id: 'hire_application_opening_id', - title: 'Opening ID', - type: 'short-input', - placeholder: 'Enter numeric opening id', - condition: { field: 'operation', value: ['hire_application'] }, - required: false, - mode: 'advanced', - }, { id: 'close_reason_id', title: 'Close Reason ID', @@ -3018,18 +2983,6 @@ export const GreenhouseV2Block: BlockConfig = { required: false, mode: 'advanced', }, - { - id: 'attachment_visibility', - title: 'Visibility', - type: 'dropdown', - condition: { field: 'operation', value: ['create_attachment', 'create_note'] }, - required: { field: 'operation', value: ['create_note'] }, - options: [ - { label: 'Admin Only', id: 'admin_only' }, - { label: 'Private', id: 'private' }, - { label: 'Public', id: 'public' }, - ], - }, { id: 'application', title: 'Application', @@ -3215,7 +3168,6 @@ export const GreenhouseV2Block: BlockConfig = { 'parent_id', 'department_id', 'office_id', - 'update_application_id', 'source_id', 'referrer_id', 'recruiter_id', @@ -3224,14 +3176,12 @@ export const GreenhouseV2Block: BlockConfig = { 'prospect_stage_id', 'candidate_id', 'job_id', - 'convert_prospect_job_id', 'initial_stage_id', 'prospect_pool_stage_id', 'prospect_owner_id', 'prospective_department_id', 'prospective_office_id', 'to_job_interview_stage_id', - 'hire_application_opening_id', 'close_reason_id', 'from_stage_id', 'to_stage_id', @@ -3264,9 +3214,6 @@ export const GreenhouseV2Block: BlockConfig = { else if (params[key] === 'false' || params[key] === false) result[key] = false else result[key] = undefined } - const remaps = GREENHOUSE_V2_PARAM_REMAPS[String(params.operation)] ?? {} - for (const [target, source] of Object.entries(remaps)) - result[target] = params[source] === '' ? undefined : (result[source] ?? params[source]) return result }, }, @@ -3274,7 +3221,10 @@ export const GreenhouseV2Block: BlockConfig = { inputs: { operation: { type: 'string', description: 'Harvest v3 operation' }, oauthCredential: { type: 'string', description: 'Connected Greenhouse OAuth credential' }, - id: { type: 'string', description: 'Greenhouse resource ID' }, + id: { + type: 'string', + description: 'Greenhouse resource ID', + }, cursor: { type: 'string', description: @@ -3285,10 +3235,10 @@ export const GreenhouseV2Block: BlockConfig = { description: 'Page size from 1 to 500 (default 100). Each call retrieves one page.', }, ids: { type: 'string', description: 'Ids Comma-separated values (maximum 50).' }, - created_at_filter: { + created_at: { type: 'json', description: - 'Created at filter as JSON with gte, lte, gt, lt keys and ISO 8601 values; cannot combine with updated_at, for example {"gte":"2026-01-01T00:00:00Z"}.', + 'Lists: JSON range using gte, lte, gt, lt with ISO 8601 timestamps; cannot combine with Updated At. Create Application: ISO 8601 timestamp. Update Application: YYYY-MM-DD date.', }, updated_at: { type: 'json', @@ -3313,7 +3263,11 @@ export const GreenhouseV2Block: BlockConfig = { type: 'string', description: 'Candidate IDs Comma-separated values (maximum 50).', }, - job_ids: { type: 'string', description: 'Job IDs Comma-separated values (maximum 50).' }, + job_ids: { + type: 'string', + description: + 'For list operations, comma-separated IDs (maximum 50). For prospect application creation, a JSON array of integer job IDs.', + }, prospective_job_ids: { type: 'string', description: 'Prospective job IDs Comma-separated values (maximum 50).', @@ -3328,9 +3282,10 @@ export const GreenhouseV2Block: BlockConfig = { description: 'Referrer IDs Comma-separated values (maximum 50).', }, stage_ids: { type: 'string', description: 'Stage IDs Comma-separated values (maximum 50).' }, - applications_status: { + status: { type: 'string', - description: 'Status Allowed: rejected, hired, converted, active.', + description: + 'Status filter for applications, interviews, jobs, offers, or scorecards. Use the values documented for the selected operation; offer statuses are case-sensitive.', }, stage_name: { type: 'string', description: 'Stage name' }, custom_field_option_id: { type: 'number', description: 'Custom field option ID' }, @@ -3348,10 +3303,10 @@ export const GreenhouseV2Block: BlockConfig = { type: 'string', description: 'Candidate tag IDs Comma-separated values (maximum 50).', }, - attachments_type: { + type: { type: 'string', description: - 'Type Allowed: resume, cover_letter, take_home_test, offer_packet, offer_letter, signed_offer_letter, other, form_attachment, midfunnel_agreement, automated_agreement.', + 'Attachment type for attachment operations; recruiter, sourcer, or coordinator for job owners; comma-separated note types for List Notes.', }, start_at: { type: 'json', @@ -3421,16 +3376,7 @@ export const GreenhouseV2Block: BlockConfig = { 'All day end on filter as JSON with gte, lte, gt, lt keys and YYYY-MM-DD dates, for example {"gte":"2026-01-01"}.', }, external_event_id: { type: 'string', description: 'External event ID' }, - interviews_status: { - type: 'string', - description: - 'Status Allowed: to_be_scheduled, scheduled, awaiting_feedback, complete, skipped, collect_feedback, to_be_sent, sent, received.', - }, user_ids: { type: 'string', description: 'User IDs Comma-separated values (maximum 50).' }, - job_owners_type: { - type: 'string', - description: 'Type Allowed: sourcer, recruiter, coordinator.', - }, opened_at: { type: 'json', description: @@ -3445,21 +3391,16 @@ export const GreenhouseV2Block: BlockConfig = { department_id: { type: 'number', description: 'Department ID' }, office_id: { type: 'number', description: 'Office ID' }, confidential: { type: 'boolean', description: 'Confidential' }, - jobs_status: { type: 'string', description: 'Status Allowed: open, draft, closed.' }, - type: { type: 'string', description: 'Type Comma-separated values (maximum 50).' }, - notes_visibility: { + visibility: { type: 'string', - description: 'Visibility Allowed: admin_only_visible, privately_visible, publicly_visible.', + description: + 'List Notes uses admin_only_visible, privately_visible, or publicly_visible. Create Note and Create Attachment use admin_only, private, or public.', }, opening_ids: { type: 'string', description: 'Opening IDs Comma-separated values (maximum 50).', }, current_only: { type: 'boolean', description: 'Current only' }, - offers_status: { - type: 'string', - description: 'Status Allowed: Created, Accepted, Rejected, Deprecated.', - }, resolved_at: { type: 'json', description: @@ -3482,7 +3423,8 @@ export const GreenhouseV2Block: BlockConfig = { open: { type: 'boolean', description: 'Open' }, opening_id: { type: 'string', - description: 'External opening ID to filter openings.', + description: + 'List Openings: external opening ID. Hire Application: numeric internal opening ID returned by List Openings.', }, prospect_pool_ids: { type: 'string', @@ -3516,7 +3458,6 @@ export const GreenhouseV2Block: BlockConfig = { description: 'Submitted at filter as JSON with gte, lte, gt, lt keys and ISO 8601 values, for example {"gte":"2026-01-01T00:00:00Z"}.', }, - scorecards_status: { type: 'string', description: 'Status Allowed: draft, complete.' }, agency_ids: { type: 'string', description: 'Agency IDs Comma-separated values (maximum 50).' }, linked_candidate_ids: { type: 'string', @@ -3535,7 +3476,6 @@ export const GreenhouseV2Block: BlockConfig = { external_office_id: { type: 'string', description: 'External office ID' }, external_department_id: { type: 'string', description: 'External department ID' }, show_service_accounts: { type: 'boolean', description: 'Show service accounts' }, - update_application_id: { type: 'number', description: 'Greenhouse resource ID' }, source_id: { type: 'number', description: 'Source ID' }, referrer_id: { type: 'number', description: 'Referrer ID' }, recruiter_id: { type: 'number', description: 'Recruiter ID' }, @@ -3543,14 +3483,6 @@ export const GreenhouseV2Block: BlockConfig = { prospect_pool_id: { type: 'number', description: 'Prospect pool ID' }, prospect_stage_id: { type: 'number', description: 'Prospect stage ID' }, rejected_at: { type: 'string', description: 'Rejected at (YYYY-MM-DD)' }, - created_at: { - type: 'string', - description: 'Application creation timestamp in ISO 8601 format', - }, - application_created_on: { - type: 'string', - description: 'Application creation date (YYYY-MM-DD)', - }, custom_fields: { type: 'array', description: @@ -3612,26 +3544,18 @@ export const GreenhouseV2Block: BlockConfig = { type: 'number', description: 'Candidate ID. Required for every note and application creation.', }, - job_id: { type: 'number', description: 'Job ID' }, - convert_prospect_job_id: { + job_id: { type: 'number', - description: 'Destination job ID for converting the prospect application', + description: + 'For application creation, required unless Prospect is true. For conversion, select a job already associated with the prospect or its pool.', }, initial_stage_id: { type: 'number', description: 'Initial stage ID' }, - create_application_job_ids: { - type: 'array', - description: 'Prospect job IDs as a JSON array of integers.', - }, prospect_pool_stage_id: { type: 'number', description: 'Prospect pool stage ID' }, prospect_owner_id: { type: 'number', description: 'Prospect owner ID' }, prospective_department_id: { type: 'number', description: 'Prospective department ID' }, prospective_office_id: { type: 'number', description: 'Prospective office ID' }, to_job_interview_stage_id: { type: 'number', description: 'To job interview stage ID' }, start_date: { type: 'string', description: 'Start date (ISO 8601)' }, - hire_application_opening_id: { - type: 'number', - description: 'Opening ID; required when hiring into a job with multiple open openings.', - }, close_reason_id: { type: 'number', description: 'Close reason ID' }, from_stage_id: { type: 'number', @@ -3667,10 +3591,6 @@ export const GreenhouseV2Block: BlockConfig = { description: 'Public attachment URL for Greenhouse to fetch. Supply exactly one of url or content.', }, - attachment_visibility: { - type: 'string', - description: 'Visibility Allowed: admin_only, private, public.', - }, application: { type: 'json', description: diff --git a/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts b/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts new file mode 100644 index 00000000000..d8a91d47320 --- /dev/null +++ b/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts @@ -0,0 +1,75 @@ +import { omit } from '@sim/utils/object' +import { getBlock } from '@/blocks' +import type { BlockState } from '@/stores/workflows/workflow/types' + +const GREENHOUSE_RETIRED_INPUTS: Record> = { + list_application_stages: { created_at: 'created_at_filter' }, + list_applications: { created_at: 'created_at_filter', status: 'applications_status' }, + list_applied_candidate_tags: { created_at: 'created_at_filter' }, + list_attachments: { created_at: 'created_at_filter', type: 'attachments_type' }, + list_candidate_educations: { created_at: 'created_at_filter' }, + list_candidate_employments: { created_at: 'created_at_filter' }, + list_candidate_tags: { created_at: 'created_at_filter' }, + list_candidates: { created_at: 'created_at_filter' }, + list_close_reasons: { created_at: 'created_at_filter' }, + list_custom_field_options: { created_at: 'created_at_filter' }, + list_custom_fields: { created_at: 'created_at_filter' }, + list_departments: { created_at: 'created_at_filter' }, + list_email_templates: { created_at: 'created_at_filter' }, + list_interviews: { created_at: 'created_at_filter', status: 'interviews_status' }, + list_job_hiring_managers: { created_at: 'created_at_filter' }, + list_job_interview_stages: { created_at: 'created_at_filter' }, + list_job_owners: { created_at: 'created_at_filter', type: 'job_owners_type' }, + list_jobs: { created_at: 'created_at_filter', status: 'jobs_status' }, + list_notes: { created_at: 'created_at_filter', visibility: 'notes_visibility' }, + list_offers: { created_at: 'created_at_filter', status: 'offers_status' }, + list_offices: { created_at: 'created_at_filter' }, + list_openings: { created_at: 'created_at_filter' }, + list_prospect_pool_stages: { created_at: 'created_at_filter' }, + list_prospect_pools: { created_at: 'created_at_filter' }, + list_referrers: { created_at: 'created_at_filter' }, + list_rejection_reasons: { created_at: 'created_at_filter' }, + list_scorecards: { created_at: 'created_at_filter', status: 'scorecards_status' }, + list_sources: { created_at: 'created_at_filter' }, + list_users: { created_at: 'created_at_filter' }, + create_attachment: { type: 'attachments_type', visibility: 'attachment_visibility' }, + update_application: { id: 'update_application_id', created_at: 'application_created_on' }, + update_candidate: { id: 'update_application_id' }, + convert_prospect: { id: 'update_application_id', job_id: 'convert_prospect_job_id' }, + hire_application: { id: 'update_application_id', opening_id: 'hire_application_opening_id' }, + move_application: { id: 'update_application_id' }, + reject_application: { id: 'update_application_id' }, + unreject_application: { id: 'update_application_id' }, + create_application: { job_ids: 'create_application_job_ids' }, + create_note: { visibility: 'attachment_visibility' }, +} + +/** Operation-scoped renames shared by the workflow migration registry and normalizer. */ +export const GREENHOUSE_SUBBLOCK_ID_MIGRATIONS = Object.entries(GREENHOUSE_RETIRED_INPUTS).flatMap( + ([operation, fields]) => + Object.entries(fields).map(([to, from]) => ({ from, to, whenOperation: [operation] })) +) + +const RETIRED_IDS = [...new Set(GREENHOUSE_SUBBLOCK_ID_MIGRATIONS.map(({ from }) => from))] + +/** + * Moves the active Harvest v3 UI aliases onto their tool parameter IDs. Canonical targets + * may already hold values from unrelated operations, so the old operation's alias wins, + * including an intentional blank. Retired aliases are all removed so changing operations + * later cannot restore a stale alias over a newly configured canonical value. + */ +export function migrateGreenhouseAgentInputs(block: BlockState): BlockState { + if (block.type !== 'greenhouse_v2') return block + const aliases = GREENHOUSE_RETIRED_INPUTS[String(block.subBlocks.operation?.value)] ?? {} + let subBlocks = block.subBlocks + for (const [target, source] of Object.entries(aliases)) { + const saved = subBlocks[source] + if (!saved) continue + const targetConfig = getBlock(block.type)?.subBlocks.find((field) => field.id === target) + if (!targetConfig) continue + subBlocks = omit(subBlocks, [source]) + subBlocks[target] = { ...saved, id: target, type: targetConfig.type } + } + if (RETIRED_IDS.some((id) => id in subBlocks)) subBlocks = omit(subBlocks, RETIRED_IDS) + return subBlocks === block.subBlocks ? block : { ...block, subBlocks } +} diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts index 5499830e9dd..8ab0740c62a 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts @@ -154,26 +154,48 @@ describe('migrateSubblockIds', () => { expect(saved.job_id.value).toBe('') expect(saved).not.toHaveProperty('update_application_id') expect(saved).not.toHaveProperty('convert_prospect_job_id') - expect(saved.created_at_filter).toEqual(block.subBlocks.created_at_filter) + expect(saved).not.toHaveProperty('created_at_filter') expect(block.subBlocks.id.value).toBe('stale-candidate') expect(migrateSubblockIds(result.blocks).migrated).toBe(false) } ) - it('leaves canonical Greenhouse fields and legacy blocks unchanged without an active retired alias', () => { - for (const type of ['greenhouse_v2', 'greenhouse']) { - const block = makeBlock({ - type, - subBlocks: { - operation: { id: 'operation', type: 'dropdown', value: 'get_candidate' }, - id: { id: 'id', type: 'short-input', value: '42' }, - update_application_id: { id: 'update_application_id', type: 'short-input', value: '99' }, - }, - }) - const result = migrateSubblockIds({ 'block-1': block }) - expect(result.migrated).toBe(false) - expect(result.blocks['block-1']).toBe(block) + it('drops inactive Greenhouse aliases before a later operation switch can overwrite a new value', () => { + const block = makeBlock({ + type: 'greenhouse_v2', + subBlocks: { + operation: { id: 'operation', type: 'dropdown', value: 'create_candidate' }, + id: { id: 'id', type: 'short-input', value: null }, + update_application_id: { id: 'update_application_id', type: 'short-input', value: '' }, + }, + }) + const first = migrateSubblockIds({ 'block-1': block }) + expect(first.blocks['block-1'].subBlocks).not.toHaveProperty('update_application_id') + const edited = { + ...first.blocks['block-1'], + subBlocks: { + ...first.blocks['block-1'].subBlocks, + operation: { id: 'operation', type: 'dropdown' as const, value: 'update_application' }, + id: { id: 'id', type: 'short-input' as const, value: '123' }, + }, } + const second = migrateSubblockIds({ 'block-1': edited }) + expect(second.blocks['block-1'].subBlocks.id.value).toBe('123') + expect(second.migrated).toBe(false) + }) + + it('leaves the legacy Greenhouse block unchanged', () => { + const block = makeBlock({ + type: 'greenhouse', + subBlocks: { + operation: { id: 'operation', type: 'dropdown', value: 'get_candidate' }, + id: { id: 'id', type: 'short-input', value: '42' }, + update_application_id: { id: 'update_application_id', type: 'short-input', value: '99' }, + }, + }) + const result = migrateSubblockIds({ 'block-1': block }) + expect(result.migrated).toBe(false) + expect(result.blocks['block-1']).toBe(block) }) it('preserves MCP canonical modes through the full normalization pipeline', () => { diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.ts index c78e57c4ea5..694c3184793 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.ts @@ -1,6 +1,10 @@ import { createLogger } from '@sim/logger' import { isPlainRecord } from '@sim/utils/object' import { DEFAULT_SUBBLOCK_TYPE } from '@sim/workflow-persistence/subblocks' +import { + GREENHOUSE_SUBBLOCK_ID_MIGRATIONS, + migrateGreenhouseAgentInputs, +} from '@/lib/workflows/migrations/greenhouse-agent-inputs' import { migrateMcpOperationControls } from '@/lib/workflows/migrations/mcp-operation-controls' import { sanitizeMalformedSubBlocks } from '@/lib/workflows/sanitization/subblocks' import { @@ -117,6 +121,8 @@ export const SUBBLOCK_ID_MIGRATIONS: Record): { continue } - const normalized = migrateMcpOperationControls(block) + const normalized = migrateGreenhouseAgentInputs(migrateMcpOperationControls(block)) const migrations = SUBBLOCK_ID_MIGRATIONS[block.type] const renamed = migrations ? migrateBlockSubblockIds(block.type, normalized.subBlocks, migrations) diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 41fa200cc38..d7e44f100f3 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -7,6 +7,7 @@ import { } from '@sim/workflow-persistence/subflow-helpers' import { resolveBlockRetryConfig } from '@sim/workflow-types/workflow' import type { Edge } from '@xyflow/react' +import { migrateGreenhouseAgentInputs } from '@/lib/workflows/migrations/greenhouse-agent-inputs' import { migrateMcpOperationControls } from '@/lib/workflows/migrations/mcp-operation-controls' import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility' import { @@ -238,7 +239,7 @@ export class Serializer { } } - block = migrateMcpOperationControls(block) + block = migrateGreenhouseAgentInputs(migrateMcpOperationControls(block)) const blockConfig = getBlock(block.type) if (!blockConfig) { throw new Error(`Invalid block type: ${block.type}`) @@ -520,7 +521,7 @@ export function selectToolId(blockConfig: any, params: Record): str * params exactly the way execution (serializeBlock) does. */ export function extractBlockParams(block: BlockState): Record { - block = migrateMcpOperationControls(block) + block = migrateGreenhouseAgentInputs(migrateMcpOperationControls(block)) if (block.type === 'loop' || block.type === 'parallel') { return {} } From 3fcb8560efca4c69d571add3180b1295f3065fd6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:08:35 -0700 Subject: [PATCH 03/11] fix(greenhouse): preserve incomplete imported block inputs --- .../migrations/greenhouse-agent-inputs.ts | 16 ++++++++++--- .../migrations/subblock-migrations.test.ts | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts b/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts index d8a91d47320..34ab6fc302a 100644 --- a/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts +++ b/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts @@ -56,16 +56,26 @@ const RETIRED_IDS = [...new Set(GREENHOUSE_SUBBLOCK_ID_MIGRATIONS.map(({ from }) * Moves the active Harvest v3 UI aliases onto their tool parameter IDs. Canonical targets * may already hold values from unrelated operations, so the old operation's alias wins, * including an intentional blank. Retired aliases are all removed so changing operations - * later cannot restore a stale alias over a newly configured canonical value. + * later cannot restore a stale alias over a newly configured canonical value. Incomplete + * imports retain their fields until a recognized operation determines which values apply. */ export function migrateGreenhouseAgentInputs(block: BlockState): BlockState { if (block.type !== 'greenhouse_v2') return block - const aliases = GREENHOUSE_RETIRED_INPUTS[String(block.subBlocks.operation?.value)] ?? {} + const config = getBlock(block.type) + const operation = block.subBlocks.operation?.value + const operations = config?.subBlocks.find((field) => field.id === 'operation')?.options + if ( + typeof operation !== 'string' || + !Array.isArray(operations) || + !operations.some((option) => option.id === operation) + ) + return block + const aliases = GREENHOUSE_RETIRED_INPUTS[operation] ?? {} let subBlocks = block.subBlocks for (const [target, source] of Object.entries(aliases)) { const saved = subBlocks[source] if (!saved) continue - const targetConfig = getBlock(block.type)?.subBlocks.find((field) => field.id === target) + const targetConfig = config?.subBlocks.find((field) => field.id === target) if (!targetConfig) continue subBlocks = omit(subBlocks, [source]) subBlocks[target] = { ...saved, id: target, type: targetConfig.type } diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts index 8ab0740c62a..6e4f1c22389 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts @@ -160,6 +160,29 @@ describe('migrateSubblockIds', () => { } ) + it.each([undefined, 'unrecognized_operation'])( + 'preserves incomplete Greenhouse imports with operation %j', + (operation) => { + const block = makeBlock({ + type: 'greenhouse_v2', + subBlocks: { + ...(operation + ? { operation: { id: 'operation', type: 'dropdown' as const, value: operation } } + : {}), + update_application_id: { id: 'update_application_id', type: 'short-input', value: '42' }, + created_at_filter: { + id: 'created_at_filter', + type: 'code', + value: '{"gte":"2026-01-01"}', + }, + }, + }) + const result = migrateSubblockIds({ 'block-1': block }) + expect(result.migrated).toBe(false) + expect(result.blocks['block-1']).toBe(block) + } + ) + it('drops inactive Greenhouse aliases before a later operation switch can overwrite a new value', () => { const block = makeBlock({ type: 'greenhouse_v2', From 3dba3d6e37aa35e2f714f8824c856a73f8da9742 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:16:45 -0700 Subject: [PATCH 04/11] fix(greenhouse): reproduce invalid configured filter choices --- apps/sim/providers/utils.test.ts | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index 22a61feb270..d4f3756a146 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -46,6 +46,10 @@ import { import { useProvidersStore } from '@/stores/providers/store' import { greenhouseConvertProspectV2Tool } from '@/tools/greenhouse/convert_prospect_v2' import { greenhouseListCandidatesV2Tool } from '@/tools/greenhouse/list_candidates_v2' +import { greenhouseListJobOwnersV2Tool } from '@/tools/greenhouse/list_job_owners_v2' +import { greenhouseListJobsV2Tool } from '@/tools/greenhouse/list_jobs_v2' +import { greenhouseListNotesV2Tool } from '@/tools/greenhouse/list_notes_v2' +import { greenhouseListOffersV2Tool } from '@/tools/greenhouse/list_offers_v2' import type { GreenhouseConvertProspectV2Params, GreenhouseListCandidatesV2Params, @@ -67,6 +71,46 @@ const mockGetRotatingApiKey = vi.fn().mockReturnValue('rotating-server-key') const originalRequire = module.require describe('integration filters through agent execution', () => { + it.each([ + { operation: 'list_jobs', field: 'status', tool: greenhouseListJobsV2Tool }, + { operation: 'list_offers', field: 'status', tool: greenhouseListOffersV2Tool }, + { operation: 'list_job_owners', field: 'type', tool: greenhouseListJobOwnersV2Tool }, + { operation: 'list_notes', field: 'visibility', tool: greenhouseListNotesV2Tool }, + ])( + 'sends every offered $operation $field choice to the API', + async ({ operation, field, tool }) => { + vi.spyOn(toolMetadata, 'getToolMetadata').mockReturnValue(tool) + const control = getSubBlocksForToolInput( + tool.id, + 'greenhouse_v2', + { operation }, + undefined, + GreenhouseV2Block + )?.subBlocks.find((item) => item.id === field) + if (!control) throw new Error('Configured filter control is unavailable') + const choices = + typeof control.options === 'function' ? control.options({ values: {} }) : control.options + if (!choices?.some((choice) => choice.id)) throw new Error('Filter choices are unavailable') + + for (const choice of choices.filter((item) => item.id)) { + const providerTool = await transformBlockTool( + { type: 'greenhouse_v2', params: { [control.id]: choice.id } }, + { + selectedOperation: operation, + getAllBlocks: () => [GreenhouseV2Block], + getTool: () => tool, + } + ) + if (!providerTool) throw new Error('Greenhouse agent tool was not created') + const { executionParams } = prepareToolExecution(providerTool, {}, {}, undefined) + const params = { ...executionParams, accessToken: 'fixture-token' } + const url = + typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url + expect(new URL(url).searchParams.get(field)).toBe(choice.id) + } + } + ) + it('sends configured Greenhouse conversion fields from the offered controls to the API', async () => { const tool = greenhouseConvertProspectV2Tool vi.spyOn(toolMetadata, 'getToolMetadata').mockReturnValue(tool) From d3a2d055139107558e68afae0306dcc444dcd008 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:20:28 -0700 Subject: [PATCH 05/11] fix(greenhouse): reproduce imported raw input loss --- .../migrations/subblock-migrations.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts index 6e4f1c22389..38226ea744c 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts @@ -126,6 +126,71 @@ describe('migration targets', () => { }) describe('migrateSubblockIds', () => { + it.each([ + { operation: { id: 'operation', type: 'dropdown', value: 'convert_prospect' }, value: '42' }, + { operation: { id: 'operation', type: 'dropdown', value: 'convert_prospect' }, value: '' }, + { operation: { id: 'operation', type: 'dropdown', value: 'convert_prospect' }, value: null }, + { operation: { id: 'operation', type: 'dropdown', value: 'convert_prospect' }, value: false }, + { operation: { id: 'operation', type: 'dropdown', value: 'convert_prospect' }, value: 0 }, + { operation: 'convert_prospect', value: '42' }, + ])( + 'preserves bare Greenhouse input $value during import normalization', + ({ operation, value }) => { + const block = makeBlock({ + type: 'greenhouse_v2', + subBlocks: { + operation, + id: { id: 'id', type: 'short-input', value: 'unrelated-candidate' }, + update_application_id: value, + convert_prospect_job_id: '', + } as BlockState['subBlocks'], + }) + const result = migrateSubblockIds({ 'block-1': block }) + const saved = result.blocks['block-1'].subBlocks + expect(saved.id.value).toBe(value) + expect(saved.job_id.value).toBe('') + expect(saved).not.toHaveProperty('update_application_id') + expect(migrateSubblockIds(result.blocks).migrated).toBe(false) + } + ) + + it('retains wrapped Greenhouse source metadata while replacing the canonical target', () => { + const source = { + id: 'update_application_id', + type: 'short-input' as const, + value: '42', + metadata: { source: { reference: 'candidate-lookup' } }, + } + const block = makeBlock({ + type: 'greenhouse_v2', + subBlocks: { + operation: { id: 'operation', type: 'dropdown', value: 'update_application' }, + id: { id: 'id', type: 'short-input', value: 'unrelated-candidate' }, + update_application_id: source, + }, + }) + const result = migrateSubblockIds({ 'block-1': block }) + expect(result.blocks['block-1'].subBlocks.id).toMatchObject({ + value: '42', + metadata: source.metadata, + }) + expect(block.subBlocks.update_application_id).toBe(source) + }) + + it('preserves a bare Greenhouse array over a stale list filter during import', () => { + const block = makeBlock({ + type: 'greenhouse_v2', + subBlocks: { + operation: { id: 'operation', type: 'dropdown', value: 'create_application' }, + job_ids: { id: 'job_ids', type: 'short-input', value: '99' }, + create_application_job_ids: [42, 81], + } as BlockState['subBlocks'], + }) + const result = migrateSubblockIds({ 'block-1': block }) + expect(result.blocks['block-1'].subBlocks.job_ids.value).toEqual([42, 81]) + expect(result.blocks['block-1'].subBlocks).not.toHaveProperty('create_application_job_ids') + }) + it.each(['42', ''])( 'preserves the active Greenhouse alias %j over an unrelated stored target', (value) => { From cd215a5840517e7ae7b25484f09d3a8075bf5b3e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:22:11 -0700 Subject: [PATCH 06/11] fix(greenhouse): preserve imported values and scope agent options --- .../migrations/greenhouse-agent-inputs.ts | 26 +++++++++++++++---- apps/sim/providers/utils.test.ts | 2 +- apps/sim/tools/params.ts | 6 ++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts b/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts index 34ab6fc302a..1646b2aeb30 100644 --- a/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts +++ b/apps/sim/lib/workflows/migrations/greenhouse-agent-inputs.ts @@ -1,4 +1,4 @@ -import { omit } from '@sim/utils/object' +import { isPlainRecord, omit } from '@sim/utils/object' import { getBlock } from '@/blocks' import type { BlockState } from '@/stores/workflows/workflow/types' @@ -62,7 +62,8 @@ const RETIRED_IDS = [...new Set(GREENHOUSE_SUBBLOCK_ID_MIGRATIONS.map(({ from }) export function migrateGreenhouseAgentInputs(block: BlockState): BlockState { if (block.type !== 'greenhouse_v2') return block const config = getBlock(block.type) - const operation = block.subBlocks.operation?.value + const storedOperation: unknown = block.subBlocks.operation + const operation = isPlainRecord(storedOperation) ? storedOperation.value : storedOperation const operations = config?.subBlocks.find((field) => field.id === 'operation')?.options if ( typeof operation !== 'string' || @@ -73,12 +74,27 @@ export function migrateGreenhouseAgentInputs(block: BlockState): BlockState { const aliases = GREENHOUSE_RETIRED_INPUTS[operation] ?? {} let subBlocks = block.subBlocks for (const [target, source] of Object.entries(aliases)) { - const saved = subBlocks[source] - if (!saved) continue + if (!Object.hasOwn(subBlocks, source)) continue + const saved: unknown = subBlocks[source] const targetConfig = config?.subBlocks.find((field) => field.id === target) if (!targetConfig) continue subBlocks = omit(subBlocks, [source]) - subBlocks[target] = { ...saved, id: target, type: targetConfig.type } + if (isPlainRecord(saved)) { + subBlocks[target] = { + ...saved, + id: target, + type: targetConfig.type, + value: (Object.hasOwn(saved, 'value') + ? saved.value + : null) as BlockState['subBlocks'][string]['value'], + } + } else { + subBlocks[target] = { + id: target, + type: targetConfig.type, + value: saved as BlockState['subBlocks'][string]['value'], + } + } } if (RETIRED_IDS.some((id) => id in subBlocks)) subBlocks = omit(subBlocks, RETIRED_IDS) return subBlocks === block.subBlocks ? block : { ...block, subBlocks } diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index d4f3756a146..cd48781013c 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -56,10 +56,10 @@ import type { } from '@/tools/greenhouse/types' import { intuneListDetectedAppsTool } from '@/tools/intune/list-detected-apps' import { intuneListDevicesTool } from '@/tools/intune/list-devices' +import * as toolMetadata from '@/tools/metadata' import { clearRangeTool } from '@/tools/microsoft_excel/clear_range' import { createTableTool } from '@/tools/microsoft_excel/create_table' import { sortRangeTool } from '@/tools/microsoft_excel/sort_range' -import * as toolMetadata from '@/tools/metadata' import { getSubBlocksForToolInput } from '@/tools/params' import { rampListReimbursementsTool } from '@/tools/ramp/list-reimbursements' import { rampListTransactionsTool } from '@/tools/ramp/list-transactions' diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index 1729248b130..8ff780bcc11 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -900,7 +900,11 @@ export function getSubBlocksForToolInput( return { toolConfig, - subBlocks: filtered, + subBlocks: filtered.map((subBlock) => + typeof subBlock.options === 'function' + ? { ...subBlock, options: subBlock.options({ values: valuesWithOperation }) } + : subBlock + ), oauthConfig: toolConfig.oauth, } } catch (error) { From 45513932b218b5e7cd6afa01104eb8127bee5790 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:25:17 -0700 Subject: [PATCH 07/11] fix(greenhouse): type legacy import regression fixtures --- .../migrations/subblock-migrations.test.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts index 38226ea744c..27a40e954c5 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts @@ -136,14 +136,15 @@ describe('migrateSubblockIds', () => { ])( 'preserves bare Greenhouse input $value during import normalization', ({ operation, value }) => { + const legacySubBlocks: Record = { + operation, + id: { id: 'id', type: 'short-input', value: 'unrelated-candidate' }, + update_application_id: value, + convert_prospect_job_id: '', + } const block = makeBlock({ type: 'greenhouse_v2', - subBlocks: { - operation, - id: { id: 'id', type: 'short-input', value: 'unrelated-candidate' }, - update_application_id: value, - convert_prospect_job_id: '', - } as BlockState['subBlocks'], + subBlocks: legacySubBlocks as BlockState['subBlocks'], }) const result = migrateSubblockIds({ 'block-1': block }) const saved = result.blocks['block-1'].subBlocks @@ -178,13 +179,14 @@ describe('migrateSubblockIds', () => { }) it('preserves a bare Greenhouse array over a stale list filter during import', () => { + const legacySubBlocks: Record = { + operation: { id: 'operation', type: 'dropdown', value: 'create_application' }, + job_ids: { id: 'job_ids', type: 'short-input', value: '99' }, + create_application_job_ids: [42, 81], + } const block = makeBlock({ type: 'greenhouse_v2', - subBlocks: { - operation: { id: 'operation', type: 'dropdown', value: 'create_application' }, - job_ids: { id: 'job_ids', type: 'short-input', value: '99' }, - create_application_job_ids: [42, 81], - } as BlockState['subBlocks'], + subBlocks: legacySubBlocks as BlockState['subBlocks'], }) const result = migrateSubblockIds({ 'block-1': block }) expect(result.blocks['block-1'].subBlocks.job_ids.value).toEqual([42, 81]) From 5038a3a6824a479ad5f502556027698e2af1f34e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:43:19 -0700 Subject: [PATCH 08/11] fix(greenhouse): reproduce incompatible operation carryover --- .../components/tool-input/param-dependents.ts | 20 ++ .../components/tool-input/tool-input.tsx | 19 +- apps/sim/providers/utils.test.ts | 186 ++++++++++++++++++ 3 files changed, 211 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts index 77e44462cf0..d2949cfd350 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts @@ -1,5 +1,8 @@ import { getTransitiveSubBlockDependents } from '@/lib/workflows/subblocks/dependencies' +import type { StoredTool } from '@/lib/workflows/tool-input/types' import { getBlock } from '@/blocks/registry' +import type { ToolMetadata } from '@/tools/metadata' +import { isUserFacingToolParam } from '@/tools/params' /** * Clear every TRANSITIVE `dependsOn` descendant of `changedParamId` in a nested tool's params, @@ -23,3 +26,20 @@ export function clearDependentToolParams( } return next ?? params } + +/** Keeps user-configured inputs that the newly selected operation accepts. */ +export function getParamsForToolOperation( + tool: Pick, + nextTool: ToolMetadata +): Record { + const newParamIds = new Set( + Object.entries(nextTool.params ?? {}) + .filter(([, param]) => isUserFacingToolParam(param)) + .map(([paramId]) => paramId) + ) + const preservedParams: Record = {} + Object.entries(tool.params || {}).forEach(([paramId, value]) => { + if (newParamIds.has(paramId) && value) preservedParams[paramId] = value + }) + return preservedParams +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 63effdd697d..9077f353d52 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -59,7 +59,10 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer' import { ToolUsageControl } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control' -import { clearDependentToolParams } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents' +import { + clearDependentToolParams, + getParamsForToolOperation, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents' import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' import { isAgentToolBlock, @@ -111,7 +114,6 @@ import { formatParameterLabel, getSubBlocksForToolInput, getToolIdForOperation, - isUserFacingToolParam, type SubBlocksForToolInput, } from '@/tools/params' @@ -979,18 +981,7 @@ export const ToolInput = memo(function ToolInput({ return } - const newParamIds = new Set( - Object.entries(newToolConfig.params ?? {}) - .filter(([, param]) => isUserFacingToolParam(param)) - .map(([paramId]) => paramId) - ) - - const preservedParams: Record = {} - Object.entries(tool.params || {}).forEach(([paramId, value]) => { - if (newParamIds.has(paramId) && value) { - preservedParams[paramId] = value - } - }) + const preservedParams = getParamsForToolOperation(tool, newToolConfig) if (tool.type === 'jira') { const subBlockStore = useSubBlockStore.getState() diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index cd48781013c..dfc8223f782 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -11,12 +11,14 @@ vi.mock('@/lib/internal/workflows/read-tool-enrichment', () => ({ readWorkflowMetadataForTool: workflowMetadataMocks.readWorkflowMetadataForTool, })) +import { getParamsForToolOperation } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents' import { GreenhouseV2Block } from '@/blocks/blocks/greenhouse' import { IntuneBlock } from '@/blocks/blocks/intune' import { MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel' import { RampBlock } from '@/blocks/blocks/ramp' import { RevenueCatBlock } from '@/blocks/blocks/revenuecat' import { VideoGeneratorV3Block } from '@/blocks/blocks/video_generator' +import * as blockRegistry from '@/blocks/registry' import { normalizeFileInput } from '@/blocks/utils' import { findProviderFromModel, @@ -45,6 +47,8 @@ import { } from '@/providers/utils' import { useProvidersStore } from '@/stores/providers/store' import { greenhouseConvertProspectV2Tool } from '@/tools/greenhouse/convert_prospect_v2' +import { greenhouseCreateApplicationV2Tool } from '@/tools/greenhouse/create_application_v2' +import { greenhouseHireApplicationV2Tool } from '@/tools/greenhouse/hire_application_v2' import { greenhouseListCandidatesV2Tool } from '@/tools/greenhouse/list_candidates_v2' import { greenhouseListJobOwnersV2Tool } from '@/tools/greenhouse/list_job_owners_v2' import { greenhouseListJobsV2Tool } from '@/tools/greenhouse/list_jobs_v2' @@ -52,8 +56,12 @@ import { greenhouseListNotesV2Tool } from '@/tools/greenhouse/list_notes_v2' import { greenhouseListOffersV2Tool } from '@/tools/greenhouse/list_offers_v2' import type { GreenhouseConvertProspectV2Params, + GreenhouseCreateApplicationV2Params, + GreenhouseHireApplicationV2Params, GreenhouseListCandidatesV2Params, + GreenhouseUpdateApplicationV2Params, } from '@/tools/greenhouse/types' +import { greenhouseUpdateApplicationV2Tool } from '@/tools/greenhouse/update_application_v2' import { intuneListDetectedAppsTool } from '@/tools/intune/list-detected-apps' import { intuneListDevicesTool } from '@/tools/intune/list-devices' import * as toolMetadata from '@/tools/metadata' @@ -71,6 +79,184 @@ const mockGetRotatingApiKey = vi.fn().mockReturnValue('rotating-server-key') const originalRequire = module.require describe('integration filters through agent execution', () => { + it.each([ + { + operation: 'list_jobs', + field: 'status', + staleValue: 'active', + tool: greenhouseListJobsV2Tool, + }, + { + operation: 'list_job_owners', + field: 'type', + staleValue: 'NOTE', + tool: greenhouseListJobOwnersV2Tool, + }, + { + operation: 'list_notes', + field: 'visibility', + staleValue: 'private', + tool: greenhouseListNotesV2Tool, + }, + { + operation: 'list_candidates', + field: 'created_at', + staleValue: '2026-01-01', + tool: greenhouseListCandidatesV2Tool, + }, + ])( + 'clears incompatible $field when switching to $operation', + async ({ operation, field, staleValue, tool }) => { + vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block) + const previousParams = { + [field]: staleValue, + per_page: '25', + credential: 'fixture-credential', + } + const params = getParamsForToolOperation( + { type: 'greenhouse_v2', params: previousParams }, + tool + ) + const providerTool = await transformBlockTool( + { type: 'greenhouse_v2', params }, + { + selectedOperation: operation, + getAllBlocks: () => [GreenhouseV2Block], + getTool: () => tool, + } + ) + if (!providerTool) throw new Error('Greenhouse agent tool was not created') + const { executionParams } = prepareToolExecution(providerTool, {}, {}, undefined) + const url = + typeof tool.request.url === 'function' + ? tool.request.url({ ...executionParams, accessToken: 'fixture-token' }) + : tool.request.url + expect(new URL(url).searchParams.has(field)).toBe(false) + expect(new URL(url).searchParams.get('per_page')).toBe('25') + expect(params.credential).toBe('fixture-credential') + expect(previousParams[field]).toBe(staleValue) + } + ) + + it('uses the newly supplied application target and fields after leaving candidate operations', async () => { + const tool = greenhouseUpdateApplicationV2Tool + vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block) + const params = getParamsForToolOperation( + { + type: 'greenhouse_v2', + params: { + id: '42', + custom_fields: '[{"custom_field_id":7,"value":"candidate value"}]', + source_id: '81', + }, + }, + tool + ) + const providerTool = await transformBlockTool( + { type: 'greenhouse_v2', params }, + { + selectedOperation: 'update_application', + getAllBlocks: () => [GreenhouseV2Block], + getTool: () => tool, + } + ) + if (!providerTool) throw new Error('Greenhouse agent tool was not created') + const { executionParams } = prepareToolExecution(providerTool, { id: 99 }, {}, undefined) + const execution = { + ...executionParams, + accessToken: 'fixture-token', + } as GreenhouseUpdateApplicationV2Params + const url = + typeof tool.request.url === 'function' ? tool.request.url(execution) : tool.request.url + expect(new URL(url).pathname).toBe('/v3/applications/99') + expect(tool.request.body?.(execution)).toEqual({ source_id: 81 }) + }) + + it('does not carry a list-filter job CSV into a prospect creation', async () => { + const tool = greenhouseCreateApplicationV2Tool + vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block) + const params = getParamsForToolOperation( + { + type: 'greenhouse_v2', + params: { + job_ids: '11,12', + candidate_id: '42', + }, + }, + tool + ) + const providerTool = await transformBlockTool( + { type: 'greenhouse_v2', params }, + { + selectedOperation: 'create_application', + getAllBlocks: () => [GreenhouseV2Block], + getTool: () => tool, + } + ) + if (!providerTool) throw new Error('Greenhouse agent tool was not created') + const { executionParams } = prepareToolExecution( + providerTool, + { prospect: true, job_ids: [81] }, + {}, + undefined + ) + expect( + tool.request.body?.({ + ...executionParams, + accessToken: 'fixture-token', + } as GreenhouseCreateApplicationV2Params) + ).toEqual({ candidate_id: 42, prospect: true, job_ids: [81] }) + }) + + it('uses a newly supplied internal opening ID after leaving opening list filters', async () => { + const tool = greenhouseHireApplicationV2Tool + vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block) + const params = getParamsForToolOperation( + { type: 'greenhouse_v2', params: { opening_id: 'external-opening' } }, + tool + ) + const providerTool = await transformBlockTool( + { type: 'greenhouse_v2', params }, + { + selectedOperation: 'hire_application', + getAllBlocks: () => [GreenhouseV2Block], + getTool: () => tool, + } + ) + if (!providerTool) throw new Error('Greenhouse agent tool was not created') + const { executionParams } = prepareToolExecution( + providerTool, + { id: 99, opening_id: 81 }, + {}, + undefined + ) + expect( + tool.request.body?.({ + ...executionParams, + accessToken: 'fixture-token', + } as GreenhouseHireApplicationV2Params) + ).toEqual({ opening_id: 81 }) + }) + + it.each(['credential', 'oauthCredential'])( + 'preserves configured %s but not a resolved token across OAuth operations', + (credentialId) => { + vi.spyOn(blockRegistry, 'getBlock').mockReturnValue(GreenhouseV2Block) + const params = getParamsForToolOperation( + { + type: 'greenhouse_v2', + params: { + [credentialId]: 'fixture-credential', + accessToken: 'expired-resolved-token', + }, + }, + greenhouseListJobsV2Tool + ) + expect(params[credentialId]).toBe('fixture-credential') + expect(params).not.toHaveProperty('accessToken') + } + ) + it.each([ { operation: 'list_jobs', field: 'status', tool: greenhouseListJobsV2Tool }, { operation: 'list_offers', field: 'status', tool: greenhouseListOffersV2Tool }, From 6c81c7f21fe231bc5559d93e1514a92090aac1fb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:47:13 -0700 Subject: [PATCH 09/11] fix(greenhouse): clear incompatible operation inputs --- .../components/tools/sub-block-renderer.tsx | 4 ++- .../components/tool-input/param-dependents.ts | 25 +++++++++++++++- .../components/tool-input/tool-input.tsx | 29 ++++++++++--------- apps/sim/blocks/blocks/greenhouse.ts | 14 +++++++++ 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx index 0b5a92b1103..2362569678e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx @@ -23,6 +23,7 @@ interface ToolSubBlockRendererProps { /** The tool's block type (e.g. `gmail`), so its params' selectors resolve dependencies. */ toolType: string toolParams: Record | undefined + operation?: string onParamChange: (toolIndex: number, paramId: string, value: string) => void disabled: boolean canonicalToggle?: { @@ -44,6 +45,7 @@ export function ToolSubBlockRenderer({ effectiveParamId, toolType, toolParams, + operation, onParamChange, disabled, canonicalToggle, @@ -128,7 +130,7 @@ export function ToolSubBlockRenderer({ isPreview={false} disabled={disabled} canonicalToggle={canonicalToggle} - dependencyContext={toolParams} + dependencyContext={{ ...toolParams, operation }} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts index d2949cfd350..5751d69b74c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/param-dependents.ts @@ -37,9 +37,32 @@ export function getParamsForToolOperation( .filter(([, param]) => isUserFacingToolParam(param)) .map(([paramId]) => paramId) ) + const subBlocks = getBlock(tool.type)?.subBlocks ?? [] + if (nextTool.oauth) { + const credentialControls = subBlocks.filter( + (subBlock) => + subBlock.type === 'oauth-input' && subBlock.serviceId === nextTool.oauth?.provider + ) + const credentialCanonicalIds = new Set( + credentialControls.flatMap((subBlock) => + subBlock.canonicalParamId ? [subBlock.canonicalParamId] : [] + ) + ) + for (const subBlock of subBlocks) { + if ( + credentialControls.includes(subBlock) || + (subBlock.canonicalParamId && + credentialCanonicalIds.has(subBlock.canonicalParamId) && + (!subBlock.serviceId || subBlock.serviceId === nextTool.oauth.provider)) + ) { + newParamIds.add(subBlock.id) + if (subBlock.canonicalParamId) newParamIds.add(subBlock.canonicalParamId) + } + } + } const preservedParams: Record = {} Object.entries(tool.params || {}).forEach(([paramId, value]) => { if (newParamIds.has(paramId) && value) preservedParams[paramId] = value }) - return preservedParams + return clearDependentToolParams(tool.type, preservedParams, 'operation') } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 9077f353d52..bd584693029 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1520,9 +1520,18 @@ export const ToolInput = memo(function ToolInput({ ? (toolBlocks.find((block) => block.type === tool.type) ?? getBlock(tool.type)) : null + const hasOperations = + !isCustomTool && !isMcpFamily && hasMultipleOperations(toolBlock ?? undefined) + const operationChoices = hasOperations + ? getOperationChoices(toolBlock ?? undefined) + : null + const selectedOperation = + tool.operation || + operationChoices?.options.find((option) => !operationChoices.denied.has(option.id))?.id + const currentToolId = !isCustomTool && !isMcpFamily - ? getToolIdForOperation(tool.type, tool.operation, toolBlock ?? undefined) || + ? getToolIdForOperation(tool.type, selectedOperation, toolBlock ?? undefined) || tool.toolId || '' : tool.toolId || '' @@ -1546,8 +1555,8 @@ export const ToolInput = memo(function ToolInput({ currentToolId, tool.type, { - operation: tool.operation, ...tool.params, + operation: selectedOperation, }, toolScopedOverrides, toolBlock ?? undefined @@ -1600,8 +1609,6 @@ export const ToolInput = memo(function ToolInput({ toolCredential?.type === sb.reactiveCondition.requiredType ) - const hasOperations = - !isCustomTool && !isMcpFamily && hasMultipleOperations(toolBlock ?? undefined) const showToolControl = supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool)) const hasToolBody = showToolControl || hasOperations || displaySubBlocks.length > 0 @@ -1860,10 +1867,8 @@ export const ToolInput = memo(function ToolInput({ )} {/* Operation dropdown for tools with multiple operations */} {(() => { - if (!hasOperations) return null - const { options: operationOptions, denied } = getOperationChoices( - toolBlock ?? undefined - ) + if (!operationChoices) return null + const { options: operationOptions, denied } = operationChoices if (operationOptions.length === 0) return null return ( @@ -1875,10 +1880,7 @@ export const ToolInput = memo(function ToolInput({ value: option.id, hidden: denied.has(option.id), }))} - value={ - tool.operation || - operationOptions.find((option) => !denied.has(option.id))?.id - } + value={selectedOperation} onChange={(value) => handleOperationChange(toolIndex, value)} placeholder='Select operation' /* Denied operations only drop out once the config @@ -1903,7 +1905,7 @@ export const ToolInput = memo(function ToolInput({ canonicalGroup && hasCanonicalPair ? resolveCanonicalMode( canonicalGroup, - { operation: tool.operation, ...tool.params }, + { ...tool.params, operation: selectedOperation }, toolScopedOverrides ) : undefined @@ -1940,6 +1942,7 @@ export const ToolInput = memo(function ToolInput({ effectiveParamId={effectiveParamId} toolType={tool.type} toolParams={tool.params} + operation={selectedOperation} onParamChange={handleParamChange} disabled={disabled} canonicalToggle={canonicalToggleProp} diff --git a/apps/sim/blocks/blocks/greenhouse.ts b/apps/sim/blocks/blocks/greenhouse.ts index c159f350493..582e93139f1 100644 --- a/apps/sim/blocks/blocks/greenhouse.ts +++ b/apps/sim/blocks/blocks/greenhouse.ts @@ -701,6 +701,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'id', + dependsOn: ['operation'], title: 'Resource ID', type: 'short-input', placeholder: 'Enter resource ID', @@ -747,6 +748,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'cursor', + dependsOn: ['operation'], title: 'Cursor', type: 'short-input', placeholder: 'Enter cursor', @@ -831,6 +833,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'ids', + dependsOn: ['operation'], title: 'Ids', type: 'short-input', placeholder: '123,456', @@ -878,6 +881,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'created_at', + dependsOn: ['operation'], title: 'Created At', type: 'long-input', placeholder: 'Date, timestamp, or JSON range for the selected operation', @@ -1021,6 +1025,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'fields', + dependsOn: ['operation'], title: 'Fields', type: 'short-input', placeholder: 'id,name', @@ -1108,6 +1113,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'job_ids', + dependsOn: ['operation'], title: 'Job IDs', type: 'long-input', placeholder: 'List filter: 123,456; prospect creation: [123,456]', @@ -1213,6 +1219,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'status', + dependsOn: ['operation'], title: 'Status', type: 'dropdown', condition: { @@ -1368,6 +1375,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'type', + dependsOn: ['operation'], title: 'Type', type: 'combobox', placeholder: 'Select a type, or enter comma-separated note types', @@ -1599,6 +1607,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'name', + dependsOn: ['operation'], title: 'Name', type: 'short-input', placeholder: 'Enter name', @@ -1616,6 +1625,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'parent_id', + dependsOn: ['operation'], title: 'Parent ID', type: 'short-input', placeholder: 'Enter numeric parent id', @@ -1625,6 +1635,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'external_id', + dependsOn: ['operation'], title: 'External ID', type: 'short-input', placeholder: 'Enter external id', @@ -1912,6 +1923,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'visibility', + dependsOn: ['operation'], title: 'Visibility', type: 'dropdown', condition: { @@ -2063,6 +2075,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'opening_id', + dependsOn: ['operation'], title: 'Opening ID', type: 'short-input', placeholder: 'Enter opening id', @@ -2394,6 +2407,7 @@ export const GreenhouseV2Block: BlockConfig = { }, { id: 'custom_fields', + dependsOn: ['operation'], title: 'Custom Fields', type: 'code', placeholder: '[]', From 1eeac11726316ba825d57ee1fca0df1d5a758d43 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 17:57:22 -0700 Subject: [PATCH 10/11] fix(editor): pass current values to required field rules --- .../editor/components/sub-block/sub-block.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 247af54d085..ab6a46a71d2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -11,6 +11,7 @@ import { import { isEqual } from 'es-toolkit' import type { FilterRule, SortRule } from '@/lib/table/query-builder/constants' import type { FallbackModelEntry } from '@/lib/workflows/blocks/fallback-models' +import { buildSubBlockValues } from '@/lib/workflows/subblocks/visibility' import { useWorkflowScope } from '@/app/workspace/[workspaceId]/providers/workflow-scope-provider' import { CheckboxList, @@ -121,10 +122,10 @@ interface SubBlockProps { * Supports boolean, condition objects, and functions that return conditions. * * @param config - The sub-block configuration containing requirement rules - * @param subBlockValues - Current values of all subblocks for condition evaluation + * @param values - Flat sibling values for condition evaluation * @returns `true` if the field is required based on current context */ -const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record): boolean => { +const isFieldRequired = (config: SubBlockConfig, values?: Record): boolean => { if (!config.required) return false if (typeof config.required === 'boolean') return config.required @@ -141,7 +142,7 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record ): boolean => { - const fieldValue = values[cond.field]?.value + const fieldValue = values[cond.field] const condValue = cond.value let match: boolean @@ -154,7 +155,7 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record, + requiredValues?: Record, wandState?: { isSearchActive: boolean searchQuery: string @@ -246,7 +248,7 @@ const renderLabel = ( if (config.type === 'switch') return null if (!config.title) return null - const required = isFieldRequired(config, subBlockValues) + const required = isFieldRequired(config, requiredValues) const showWand = wandState?.isWandEnabled && !wandState.isPreview && !wandState.disabled const showCanonicalToggle = !!canonicalToggle && !wandState?.isPreview const showCopy = copyState?.showCopyButton && !wandState?.isPreview @@ -617,6 +619,8 @@ function SubBlockComponent({ | null | undefined + const requiredValues = + dependencyContext ?? (subBlockValues ? buildSubBlockValues(subBlockValues) : undefined) const contextValues = dependencyContext ?? (isPreview ? subBlockValues : undefined) const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, { @@ -956,7 +960,7 @@ function SubBlockComponent({ blockId={blockId} subBlock={config} disabled={isDisabled} - required={isFieldRequired(config, subBlockValues)} + required={isFieldRequired(config, requiredValues)} isPreview={isPreview} previewValue={previewValue} /> @@ -1248,7 +1252,7 @@ function SubBlockComponent({ {renderLabel( config, isValidJson, - subBlockValues, + requiredValues, { isSearchActive, searchQuery, From b5b5658b5858f8b67bc15f7992906e614db9cedd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 18:07:31 -0700 Subject: [PATCH 11/11] fix(editor): preserve preview required field context --- .../components/editor/components/sub-block/sub-block.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index ab6a46a71d2..9c7b014571a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -620,7 +620,11 @@ function SubBlockComponent({ | undefined const requiredValues = - dependencyContext ?? (subBlockValues ? buildSubBlockValues(subBlockValues) : undefined) + !isPreview && dependencyContext + ? dependencyContext + : subBlockValues + ? buildSubBlockValues(subBlockValues) + : undefined const contextValues = dependencyContext ?? (isPreview ? subBlockValues : undefined) const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, {