From 4761ede9fa7818f48440fc6e7a3f58ce73207d42 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 10:51:46 -0700 Subject: [PATCH 1/3] feat(triggers): add Greenhouse webhook triggers Add 8 webhook triggers for Greenhouse ATS events: - Candidate Hired, New Application, Stage Change, Rejected - Offer Created, Job Created, Job Updated - Generic Webhook (all events) Includes event filtering via provider handler registry and output schemas matching actual Greenhouse webhook payload structures. --- apps/sim/blocks/blocks/greenhouse.ts | 26 ++ apps/sim/lib/webhooks/providers/greenhouse.ts | 48 +++ apps/sim/lib/webhooks/providers/registry.ts | 2 + .../triggers/greenhouse/candidate_hired.ts | 39 +++ .../triggers/greenhouse/candidate_rejected.ts | 37 ++ .../greenhouse/candidate_stage_change.ts | 37 ++ apps/sim/triggers/greenhouse/index.ts | 8 + apps/sim/triggers/greenhouse/job_created.ts | 37 ++ apps/sim/triggers/greenhouse/job_updated.ts | 37 ++ .../triggers/greenhouse/new_application.ts | 37 ++ apps/sim/triggers/greenhouse/offer_created.ts | 37 ++ apps/sim/triggers/greenhouse/utils.ts | 321 ++++++++++++++++++ apps/sim/triggers/greenhouse/webhook.ts | 37 ++ apps/sim/triggers/registry.ts | 18 + 14 files changed, 721 insertions(+) create mode 100644 apps/sim/lib/webhooks/providers/greenhouse.ts create mode 100644 apps/sim/triggers/greenhouse/candidate_hired.ts create mode 100644 apps/sim/triggers/greenhouse/candidate_rejected.ts create mode 100644 apps/sim/triggers/greenhouse/candidate_stage_change.ts create mode 100644 apps/sim/triggers/greenhouse/index.ts create mode 100644 apps/sim/triggers/greenhouse/job_created.ts create mode 100644 apps/sim/triggers/greenhouse/job_updated.ts create mode 100644 apps/sim/triggers/greenhouse/new_application.ts create mode 100644 apps/sim/triggers/greenhouse/offer_created.ts create mode 100644 apps/sim/triggers/greenhouse/utils.ts create mode 100644 apps/sim/triggers/greenhouse/webhook.ts diff --git a/apps/sim/blocks/blocks/greenhouse.ts b/apps/sim/blocks/blocks/greenhouse.ts index 4df4963fca4..4df34ef87cb 100644 --- a/apps/sim/blocks/blocks/greenhouse.ts +++ b/apps/sim/blocks/blocks/greenhouse.ts @@ -1,6 +1,7 @@ import { GreenhouseIcon } from '@/components/icons' import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' import type { GreenhouseResponse } from '@/tools/greenhouse/types' +import { getTrigger } from '@/triggers' export const GreenhouseBlock: BlockConfig = { type: 'greenhouse', @@ -16,6 +17,20 @@ export const GreenhouseBlock: BlockConfig = { icon: GreenhouseIcon, authMode: AuthMode.ApiKey, + triggers: { + enabled: true, + available: [ + 'greenhouse_candidate_hired', + 'greenhouse_new_application', + 'greenhouse_candidate_stage_change', + 'greenhouse_candidate_rejected', + 'greenhouse_offer_created', + 'greenhouse_job_created', + 'greenhouse_job_updated', + 'greenhouse_webhook', + ], + }, + subBlocks: [ { id: 'operation', @@ -291,6 +306,17 @@ Return ONLY the ISO 8601 timestamp - no explanations, no extra text.`, required: true, password: true, }, + + // ── Trigger subBlocks ── + + ...getTrigger('greenhouse_candidate_hired').subBlocks, + ...getTrigger('greenhouse_new_application').subBlocks, + ...getTrigger('greenhouse_candidate_stage_change').subBlocks, + ...getTrigger('greenhouse_candidate_rejected').subBlocks, + ...getTrigger('greenhouse_offer_created').subBlocks, + ...getTrigger('greenhouse_job_created').subBlocks, + ...getTrigger('greenhouse_job_updated').subBlocks, + ...getTrigger('greenhouse_webhook').subBlocks, ], tools: { diff --git a/apps/sim/lib/webhooks/providers/greenhouse.ts b/apps/sim/lib/webhooks/providers/greenhouse.ts new file mode 100644 index 00000000000..5433ac1d82b --- /dev/null +++ b/apps/sim/lib/webhooks/providers/greenhouse.ts @@ -0,0 +1,48 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import type { + EventMatchContext, + FormatInputContext, + FormatInputResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' + +const logger = createLogger('WebhookProvider:Greenhouse') + +export const greenhouseHandler: WebhookProviderHandler = { + async formatInput({ body }: FormatInputContext): Promise { + const b = body as Record + return { + input: { + action: b.action, + payload: b.payload || {}, + }, + } + }, + + async matchEvent({ webhook, body, requestId, providerConfig }: EventMatchContext) { + const triggerId = providerConfig.triggerId as string | undefined + const b = body as Record + const action = b.action as string | undefined + + if (triggerId && triggerId !== 'greenhouse_webhook') { + const { isGreenhouseEventMatch } = await import('@/triggers/greenhouse/utils') + if (!isGreenhouseEventMatch(triggerId, action || '')) { + logger.debug( + `[${requestId}] Greenhouse event mismatch for trigger ${triggerId}. Action: ${action}. Skipping execution.`, + { + webhookId: webhook.id, + triggerId, + receivedAction: action, + } + ) + + return NextResponse.json({ + message: 'Event type does not match trigger configuration. Ignoring.', + }) + } + } + + return true + }, +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 00ae58a21b1..f93dc478c97 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -14,6 +14,7 @@ import { githubHandler } from '@/lib/webhooks/providers/github' import { gmailHandler } from '@/lib/webhooks/providers/gmail' import { googleFormsHandler } from '@/lib/webhooks/providers/google-forms' import { grainHandler } from '@/lib/webhooks/providers/grain' +import { greenhouseHandler } from '@/lib/webhooks/providers/greenhouse' import { hubspotHandler } from '@/lib/webhooks/providers/hubspot' import { imapHandler } from '@/lib/webhooks/providers/imap' import { jiraHandler } from '@/lib/webhooks/providers/jira' @@ -50,6 +51,7 @@ const PROVIDER_HANDLERS: Record = { google_forms: googleFormsHandler, fathom: fathomHandler, grain: grainHandler, + greenhouse: greenhouseHandler, hubspot: hubspotHandler, imap: imapHandler, jira: jiraHandler, diff --git a/apps/sim/triggers/greenhouse/candidate_hired.ts b/apps/sim/triggers/greenhouse/candidate_hired.ts new file mode 100644 index 00000000000..41391297b3e --- /dev/null +++ b/apps/sim/triggers/greenhouse/candidate_hired.ts @@ -0,0 +1,39 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildCandidateHiredOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse Candidate Hired Trigger + * + * This is the PRIMARY trigger - it includes the dropdown for selecting trigger type. + * Fires when a candidate is marked as hired in Greenhouse. + */ +export const greenhouseCandidateHiredTrigger: TriggerConfig = { + id: 'greenhouse_candidate_hired', + name: 'Greenhouse Candidate Hired', + provider: 'greenhouse', + description: 'Trigger workflow when a candidate is hired', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_candidate_hired', + triggerOptions: greenhouseTriggerOptions, + includeDropdown: true, + setupInstructions: greenhouseSetupInstructions('Candidate Hired'), + }), + + outputs: buildCandidateHiredOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/greenhouse/candidate_rejected.ts b/apps/sim/triggers/greenhouse/candidate_rejected.ts new file mode 100644 index 00000000000..119f24c6f60 --- /dev/null +++ b/apps/sim/triggers/greenhouse/candidate_rejected.ts @@ -0,0 +1,37 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildCandidateRejectedOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse Candidate Rejected Trigger + * + * Fires when a candidate is rejected from a position. + */ +export const greenhouseCandidateRejectedTrigger: TriggerConfig = { + id: 'greenhouse_candidate_rejected', + name: 'Greenhouse Candidate Rejected', + provider: 'greenhouse', + description: 'Trigger workflow when a candidate is rejected', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_candidate_rejected', + triggerOptions: greenhouseTriggerOptions, + setupInstructions: greenhouseSetupInstructions('Candidate Rejected'), + }), + + outputs: buildCandidateRejectedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/greenhouse/candidate_stage_change.ts b/apps/sim/triggers/greenhouse/candidate_stage_change.ts new file mode 100644 index 00000000000..dffd7df656b --- /dev/null +++ b/apps/sim/triggers/greenhouse/candidate_stage_change.ts @@ -0,0 +1,37 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildCandidateStageChangeOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse Candidate Stage Change Trigger + * + * Fires when a candidate moves to a different interview stage. + */ +export const greenhouseCandidateStageChangeTrigger: TriggerConfig = { + id: 'greenhouse_candidate_stage_change', + name: 'Greenhouse Candidate Stage Change', + provider: 'greenhouse', + description: 'Trigger workflow when a candidate changes interview stages', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_candidate_stage_change', + triggerOptions: greenhouseTriggerOptions, + setupInstructions: greenhouseSetupInstructions('Candidate Stage Change'), + }), + + outputs: buildCandidateStageChangeOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/greenhouse/index.ts b/apps/sim/triggers/greenhouse/index.ts new file mode 100644 index 00000000000..e9508aa154a --- /dev/null +++ b/apps/sim/triggers/greenhouse/index.ts @@ -0,0 +1,8 @@ +export { greenhouseCandidateHiredTrigger } from './candidate_hired' +export { greenhouseCandidateRejectedTrigger } from './candidate_rejected' +export { greenhouseCandidateStageChangeTrigger } from './candidate_stage_change' +export { greenhouseJobCreatedTrigger } from './job_created' +export { greenhouseJobUpdatedTrigger } from './job_updated' +export { greenhouseNewApplicationTrigger } from './new_application' +export { greenhouseOfferCreatedTrigger } from './offer_created' +export { greenhouseWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/greenhouse/job_created.ts b/apps/sim/triggers/greenhouse/job_created.ts new file mode 100644 index 00000000000..17e8ff745b8 --- /dev/null +++ b/apps/sim/triggers/greenhouse/job_created.ts @@ -0,0 +1,37 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildJobCreatedOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse Job Created Trigger + * + * Fires when a new job posting is created. + */ +export const greenhouseJobCreatedTrigger: TriggerConfig = { + id: 'greenhouse_job_created', + name: 'Greenhouse Job Created', + provider: 'greenhouse', + description: 'Trigger workflow when a new job is created', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_job_created', + triggerOptions: greenhouseTriggerOptions, + setupInstructions: greenhouseSetupInstructions('Job Created'), + }), + + outputs: buildJobCreatedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/greenhouse/job_updated.ts b/apps/sim/triggers/greenhouse/job_updated.ts new file mode 100644 index 00000000000..8ca6cdb678e --- /dev/null +++ b/apps/sim/triggers/greenhouse/job_updated.ts @@ -0,0 +1,37 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildJobUpdatedOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse Job Updated Trigger + * + * Fires when a job posting is updated. + */ +export const greenhouseJobUpdatedTrigger: TriggerConfig = { + id: 'greenhouse_job_updated', + name: 'Greenhouse Job Updated', + provider: 'greenhouse', + description: 'Trigger workflow when a job is updated', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_job_updated', + triggerOptions: greenhouseTriggerOptions, + setupInstructions: greenhouseSetupInstructions('Job Updated'), + }), + + outputs: buildJobUpdatedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/greenhouse/new_application.ts b/apps/sim/triggers/greenhouse/new_application.ts new file mode 100644 index 00000000000..f09a928865a --- /dev/null +++ b/apps/sim/triggers/greenhouse/new_application.ts @@ -0,0 +1,37 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNewApplicationOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse New Application Trigger + * + * Fires when a new candidate application is submitted. + */ +export const greenhouseNewApplicationTrigger: TriggerConfig = { + id: 'greenhouse_new_application', + name: 'Greenhouse New Application', + provider: 'greenhouse', + description: 'Trigger workflow when a new application is submitted', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_new_application', + triggerOptions: greenhouseTriggerOptions, + setupInstructions: greenhouseSetupInstructions('New Candidate Application'), + }), + + outputs: buildNewApplicationOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/greenhouse/offer_created.ts b/apps/sim/triggers/greenhouse/offer_created.ts new file mode 100644 index 00000000000..2de60d342a4 --- /dev/null +++ b/apps/sim/triggers/greenhouse/offer_created.ts @@ -0,0 +1,37 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildOfferCreatedOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse Offer Created Trigger + * + * Fires when a new offer is created for a candidate. + */ +export const greenhouseOfferCreatedTrigger: TriggerConfig = { + id: 'greenhouse_offer_created', + name: 'Greenhouse Offer Created', + provider: 'greenhouse', + description: 'Trigger workflow when a new offer is created', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_offer_created', + triggerOptions: greenhouseTriggerOptions, + setupInstructions: greenhouseSetupInstructions('Offer Created'), + }), + + outputs: buildOfferCreatedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/greenhouse/utils.ts b/apps/sim/triggers/greenhouse/utils.ts new file mode 100644 index 00000000000..7d6169c053b --- /dev/null +++ b/apps/sim/triggers/greenhouse/utils.ts @@ -0,0 +1,321 @@ +import type { TriggerOutput } from '@/triggers/types' + +/** + * Dropdown options for the Greenhouse trigger type selector. + */ +export const greenhouseTriggerOptions = [ + { label: 'Candidate Hired', id: 'greenhouse_candidate_hired' }, + { label: 'New Application', id: 'greenhouse_new_application' }, + { label: 'Candidate Stage Change', id: 'greenhouse_candidate_stage_change' }, + { label: 'Candidate Rejected', id: 'greenhouse_candidate_rejected' }, + { label: 'Offer Created', id: 'greenhouse_offer_created' }, + { label: 'Job Created', id: 'greenhouse_job_created' }, + { label: 'Job Updated', id: 'greenhouse_job_updated' }, + { label: 'Generic Webhook (All Events)', id: 'greenhouse_webhook' }, +] + +/** + * Maps trigger IDs to Greenhouse webhook action strings. + * Used for event filtering in the webhook processor. + */ +export const GREENHOUSE_EVENT_MAP: Record = { + greenhouse_candidate_hired: 'hire_candidate', + greenhouse_new_application: 'new_candidate_application', + greenhouse_candidate_stage_change: 'candidate_stage_change', + greenhouse_candidate_rejected: 'reject_candidate', + greenhouse_offer_created: 'offer_created', + greenhouse_job_created: 'job_created', + greenhouse_job_updated: 'job_updated', +} + +/** + * Checks whether a Greenhouse webhook payload matches the configured trigger. + */ +export function isGreenhouseEventMatch(triggerId: string, action: string): boolean { + const expectedAction = GREENHOUSE_EVENT_MAP[triggerId] + if (!expectedAction) { + return true + } + return action === expectedAction +} + +/** + * Generates HTML setup instructions for Greenhouse webhooks. + * Webhooks are manually configured in the Greenhouse admin panel. + */ +export function greenhouseSetupInstructions(eventType: string): string { + const instructions = [ + 'Copy the Webhook URL above.', + 'In Greenhouse, go to Configure > Dev Center > Webhooks.', + 'Click Create New Webhook.', + 'Paste the Webhook URL into the Endpoint URL field.', + 'Enter a Secret Key for signature verification (optional).', + `Under When, select the ${eventType} event.`, + 'Click Create Webhook to save.', + 'Click "Save" above to activate your trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Build outputs for hire_candidate events. + * Greenhouse nests candidate inside application: payload.application.candidate + * Uses both singular `job` (deprecated) and `jobs` array. + */ +export function buildCandidateHiredOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type (hire_candidate)' }, + payload: { + application: { + id: { type: 'number', description: 'Application ID' }, + status: { type: 'string', description: 'Application status' }, + prospect: { type: 'boolean', description: 'Whether the applicant is a prospect' }, + applied_at: { type: 'string', description: 'When the application was submitted' }, + url: { type: 'string', description: 'Application URL in Greenhouse' }, + current_stage: { + id: { type: 'number', description: 'Current stage ID' }, + name: { type: 'string', description: 'Current stage name' }, + }, + candidate: { + id: { type: 'number', description: 'Candidate ID' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + title: { type: 'string', description: 'Current title' }, + company: { type: 'string', description: 'Current company' }, + email_addresses: { type: 'json', description: 'Email addresses' }, + phone_numbers: { type: 'json', description: 'Phone numbers' }, + recruiter: { type: 'json', description: 'Assigned recruiter' }, + coordinator: { type: 'json', description: 'Assigned coordinator' }, + }, + jobs: { type: 'json', description: 'Associated jobs (array)' }, + source: { + id: { type: 'number', description: 'Source ID' }, + public_name: { type: 'string', description: 'Source name' }, + }, + offer: { + id: { type: 'number', description: 'Offer ID' }, + version: { type: 'number', description: 'Offer version' }, + starts_at: { type: 'string', description: 'Offer start date' }, + custom_fields: { type: 'json', description: 'Offer custom fields' }, + }, + custom_fields: { type: 'json', description: 'Application custom fields' }, + }, + }, + } as Record +} + +/** + * Build outputs for new_candidate_application events. + * Candidate is nested inside application: payload.application.candidate + */ +export function buildNewApplicationOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type (new_candidate_application)' }, + payload: { + application: { + id: { type: 'number', description: 'Application ID' }, + status: { type: 'string', description: 'Application status' }, + prospect: { type: 'boolean', description: 'Whether the applicant is a prospect' }, + applied_at: { type: 'string', description: 'When the application was submitted' }, + url: { type: 'string', description: 'Application URL in Greenhouse' }, + current_stage: { + id: { type: 'number', description: 'Current stage ID' }, + name: { type: 'string', description: 'Current stage name' }, + }, + candidate: { + id: { type: 'number', description: 'Candidate ID' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + title: { type: 'string', description: 'Current title' }, + company: { type: 'string', description: 'Current company' }, + created_at: { type: 'string', description: 'When the candidate was created' }, + email_addresses: { type: 'json', description: 'Email addresses' }, + phone_numbers: { type: 'json', description: 'Phone numbers' }, + tags: { type: 'json', description: 'Candidate tags' }, + }, + jobs: { type: 'json', description: 'Associated jobs (array)' }, + source: { + id: { type: 'number', description: 'Source ID' }, + public_name: { type: 'string', description: 'Source name' }, + }, + answers: { type: 'json', description: 'Application question answers' }, + attachments: { type: 'json', description: 'Application attachments' }, + custom_fields: { type: 'json', description: 'Application custom fields' }, + }, + }, + } as Record +} + +/** + * Build outputs for candidate_stage_change events. + * Candidate is nested inside application: payload.application.candidate + */ +export function buildCandidateStageChangeOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type (candidate_stage_change)' }, + payload: { + application: { + id: { type: 'number', description: 'Application ID' }, + status: { type: 'string', description: 'Application status' }, + prospect: { type: 'boolean', description: 'Whether the applicant is a prospect' }, + applied_at: { type: 'string', description: 'When the application was submitted' }, + url: { type: 'string', description: 'Application URL in Greenhouse' }, + current_stage: { + id: { type: 'number', description: 'Current stage ID' }, + name: { type: 'string', description: 'Current stage name' }, + interviews: { type: 'json', description: 'Interviews in this stage' }, + }, + candidate: { + id: { type: 'number', description: 'Candidate ID' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + title: { type: 'string', description: 'Current title' }, + company: { type: 'string', description: 'Current company' }, + email_addresses: { type: 'json', description: 'Email addresses' }, + phone_numbers: { type: 'json', description: 'Phone numbers' }, + }, + jobs: { type: 'json', description: 'Associated jobs (array)' }, + source: { + id: { type: 'number', description: 'Source ID' }, + public_name: { type: 'string', description: 'Source name' }, + }, + custom_fields: { type: 'json', description: 'Application custom fields' }, + }, + }, + } as Record +} + +/** + * Build outputs for reject_candidate events. + * Candidate is nested inside application: payload.application.candidate + */ +export function buildCandidateRejectedOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type (reject_candidate)' }, + payload: { + application: { + id: { type: 'number', description: 'Application ID' }, + status: { type: 'string', description: 'Application status (rejected)' }, + prospect: { type: 'boolean', description: 'Whether the applicant is a prospect' }, + applied_at: { type: 'string', description: 'When the application was submitted' }, + rejected_at: { type: 'string', description: 'When the candidate was rejected' }, + url: { type: 'string', description: 'Application URL in Greenhouse' }, + current_stage: { + id: { type: 'number', description: 'Stage ID where rejected' }, + name: { type: 'string', description: 'Stage name where rejected' }, + }, + candidate: { + id: { type: 'number', description: 'Candidate ID' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + email_addresses: { type: 'json', description: 'Email addresses' }, + phone_numbers: { type: 'json', description: 'Phone numbers' }, + }, + jobs: { type: 'json', description: 'Associated jobs (array)' }, + rejection_reason: { + id: { type: 'number', description: 'Rejection reason ID' }, + name: { type: 'string', description: 'Rejection reason name' }, + type: { + id: { type: 'number', description: 'Rejection reason type ID' }, + name: { type: 'string', description: 'Rejection reason type name' }, + }, + }, + rejection_details: { type: 'json', description: 'Rejection details with custom fields' }, + custom_fields: { type: 'json', description: 'Application custom fields' }, + }, + }, + } as Record +} + +/** + * Build outputs for offer_created events. + * Offer payload is flat under payload (not nested under payload.offer). + */ +export function buildOfferCreatedOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type (offer_created)' }, + payload: { + id: { type: 'number', description: 'Offer ID' }, + application_id: { type: 'number', description: 'Associated application ID' }, + job_id: { type: 'number', description: 'Associated job ID' }, + user_id: { type: 'number', description: 'User who created the offer' }, + version: { type: 'number', description: 'Offer version number' }, + sent_on: { type: 'string', description: 'When the offer was sent' }, + resolved_at: { type: 'string', description: 'When the offer was resolved' }, + start_date: { type: 'string', description: 'Offer start date' }, + notes: { type: 'string', description: 'Offer notes' }, + offer_status: { type: 'string', description: 'Offer status' }, + custom_fields: { type: 'json', description: 'Custom field values' }, + }, + } as Record +} + +/** + * Build outputs for job_created events. + * Job data is nested under payload.job. + */ +export function buildJobCreatedOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type (job_created)' }, + payload: { + job: { + id: { type: 'number', description: 'Job ID' }, + name: { type: 'string', description: 'Job title' }, + requisition_id: { type: 'string', description: 'Requisition ID' }, + status: { type: 'string', description: 'Job status (open, closed, draft)' }, + confidential: { type: 'boolean', description: 'Whether the job is confidential' }, + created_at: { type: 'string', description: 'When the job was created' }, + opened_at: { type: 'string', description: 'When the job was opened' }, + closed_at: { type: 'string', description: 'When the job was closed' }, + departments: { type: 'json', description: 'Associated departments' }, + offices: { type: 'json', description: 'Associated offices' }, + hiring_team: { type: 'json', description: 'Hiring team (managers, recruiters, etc.)' }, + openings: { type: 'json', description: 'Job openings' }, + custom_fields: { type: 'json', description: 'Custom field values' }, + }, + }, + } as Record +} + +/** + * Build outputs for job_updated events. + * Same structure as job_created. + */ +export function buildJobUpdatedOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type (job_updated)' }, + payload: { + job: { + id: { type: 'number', description: 'Job ID' }, + name: { type: 'string', description: 'Job title' }, + requisition_id: { type: 'string', description: 'Requisition ID' }, + status: { type: 'string', description: 'Job status (open, closed, draft)' }, + confidential: { type: 'boolean', description: 'Whether the job is confidential' }, + created_at: { type: 'string', description: 'When the job was created' }, + opened_at: { type: 'string', description: 'When the job was opened' }, + closed_at: { type: 'string', description: 'When the job was closed' }, + departments: { type: 'json', description: 'Associated departments' }, + offices: { type: 'json', description: 'Associated offices' }, + hiring_team: { type: 'json', description: 'Hiring team (managers, recruiters, etc.)' }, + openings: { type: 'json', description: 'Job openings' }, + custom_fields: { type: 'json', description: 'Custom field values' }, + }, + }, + } as Record +} + +/** + * Build outputs for generic webhook (all events). + */ +export function buildWebhookOutputs(): Record { + return { + action: { type: 'string', description: 'The webhook event type' }, + payload: { type: 'json', description: 'Full event payload' }, + } as Record +} diff --git a/apps/sim/triggers/greenhouse/webhook.ts b/apps/sim/triggers/greenhouse/webhook.ts new file mode 100644 index 00000000000..7cf39565a09 --- /dev/null +++ b/apps/sim/triggers/greenhouse/webhook.ts @@ -0,0 +1,37 @@ +import { GreenhouseIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildWebhookOutputs, + greenhouseSetupInstructions, + greenhouseTriggerOptions, +} from '@/triggers/greenhouse/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Greenhouse Generic Webhook Trigger + * + * Accepts all Greenhouse webhook events without filtering. + */ +export const greenhouseWebhookTrigger: TriggerConfig = { + id: 'greenhouse_webhook', + name: 'Greenhouse Webhook (All Events)', + provider: 'greenhouse', + description: 'Trigger workflow on any Greenhouse webhook event', + version: '1.0.0', + icon: GreenhouseIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'greenhouse_webhook', + triggerOptions: greenhouseTriggerOptions, + setupInstructions: greenhouseSetupInstructions('All Events'), + }), + + outputs: buildWebhookOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 4390bfeefff..680cc2dc03f 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -100,6 +100,16 @@ import { grainStoryCreatedTrigger, grainWebhookTrigger, } from '@/triggers/grain' +import { + greenhouseCandidateHiredTrigger, + greenhouseCandidateRejectedTrigger, + greenhouseCandidateStageChangeTrigger, + greenhouseJobCreatedTrigger, + greenhouseJobUpdatedTrigger, + greenhouseNewApplicationTrigger, + greenhouseOfferCreatedTrigger, + greenhouseWebhookTrigger, +} from '@/triggers/greenhouse' import { hubspotCompanyCreatedTrigger, hubspotCompanyDeletedTrigger, @@ -238,6 +248,14 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { confluence_label_added: confluenceLabelAddedTrigger, confluence_label_removed: confluenceLabelRemovedTrigger, generic_webhook: genericWebhookTrigger, + greenhouse_candidate_hired: greenhouseCandidateHiredTrigger, + greenhouse_new_application: greenhouseNewApplicationTrigger, + greenhouse_candidate_stage_change: greenhouseCandidateStageChangeTrigger, + greenhouse_candidate_rejected: greenhouseCandidateRejectedTrigger, + greenhouse_offer_created: greenhouseOfferCreatedTrigger, + greenhouse_job_created: greenhouseJobCreatedTrigger, + greenhouse_job_updated: greenhouseJobUpdatedTrigger, + greenhouse_webhook: greenhouseWebhookTrigger, github_webhook: githubWebhookTrigger, github_issue_opened: githubIssueOpenedTrigger, github_issue_closed: githubIssueClosedTrigger, From 8af90164658d8fcb8894e123512e4431635aea1c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 11:05:34 -0700 Subject: [PATCH 2/3] fix(triggers): address PR review feedback for Greenhouse triggers - Fix rejection_reason.type key collision with mock payload generator by renaming to reason_type - Replace dynamic import with static import in matchEvent handler - Add HMAC-SHA256 signature verification via createHmacVerifier - Add secretKey extra field to all trigger subBlocks - Extract shared buildJobPayload helper to deduplicate job outputs --- apps/sim/lib/webhooks/providers/greenhouse.ts | 34 +++++++- .../triggers/greenhouse/candidate_hired.ts | 2 + .../triggers/greenhouse/candidate_rejected.ts | 2 + .../greenhouse/candidate_stage_change.ts | 2 + apps/sim/triggers/greenhouse/job_created.ts | 2 + apps/sim/triggers/greenhouse/job_updated.ts | 2 + .../triggers/greenhouse/new_application.ts | 2 + apps/sim/triggers/greenhouse/offer_created.ts | 2 + apps/sim/triggers/greenhouse/utils.ts | 79 +++++++++++-------- apps/sim/triggers/greenhouse/webhook.ts | 2 + 10 files changed, 93 insertions(+), 36 deletions(-) diff --git a/apps/sim/lib/webhooks/providers/greenhouse.ts b/apps/sim/lib/webhooks/providers/greenhouse.ts index 5433ac1d82b..65f3090dee8 100644 --- a/apps/sim/lib/webhooks/providers/greenhouse.ts +++ b/apps/sim/lib/webhooks/providers/greenhouse.ts @@ -1,15 +1,48 @@ +import crypto from 'crypto' import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' +import { safeCompare } from '@/lib/core/security/encryption' import type { EventMatchContext, FormatInputContext, FormatInputResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' +import { createHmacVerifier } from '@/lib/webhooks/providers/utils' +import { isGreenhouseEventMatch } from '@/triggers/greenhouse/utils' const logger = createLogger('WebhookProvider:Greenhouse') +/** + * Validates the Greenhouse HMAC-SHA256 signature. + * Greenhouse sends: `Signature: sha256 ` + */ +function validateGreenhouseSignature(secretKey: string, signature: string, body: string): boolean { + try { + if (!secretKey || !signature || !body) { + return false + } + const prefix = 'sha256 ' + if (!signature.startsWith(prefix)) { + return false + } + const providedDigest = signature.substring(prefix.length) + const computedDigest = crypto.createHmac('sha256', secretKey).update(body, 'utf8').digest('hex') + return safeCompare(computedDigest, providedDigest) + } catch { + logger.error('Error validating Greenhouse signature') + return false + } +} + export const greenhouseHandler: WebhookProviderHandler = { + verifyAuth: createHmacVerifier({ + configKey: 'secretKey', + headerName: 'signature', + validateFn: validateGreenhouseSignature, + providerLabel: 'Greenhouse', + }), + async formatInput({ body }: FormatInputContext): Promise { const b = body as Record return { @@ -26,7 +59,6 @@ export const greenhouseHandler: WebhookProviderHandler = { const action = b.action as string | undefined if (triggerId && triggerId !== 'greenhouse_webhook') { - const { isGreenhouseEventMatch } = await import('@/triggers/greenhouse/utils') if (!isGreenhouseEventMatch(triggerId, action || '')) { logger.debug( `[${requestId}] Greenhouse event mismatch for trigger ${triggerId}. Action: ${action}. Skipping execution.`, diff --git a/apps/sim/triggers/greenhouse/candidate_hired.ts b/apps/sim/triggers/greenhouse/candidate_hired.ts index 41391297b3e..0805bddcbbe 100644 --- a/apps/sim/triggers/greenhouse/candidate_hired.ts +++ b/apps/sim/triggers/greenhouse/candidate_hired.ts @@ -2,6 +2,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { buildCandidateHiredOutputs, + buildGreenhouseExtraFields, greenhouseSetupInstructions, greenhouseTriggerOptions, } from '@/triggers/greenhouse/utils' @@ -26,6 +27,7 @@ export const greenhouseCandidateHiredTrigger: TriggerConfig = { triggerOptions: greenhouseTriggerOptions, includeDropdown: true, setupInstructions: greenhouseSetupInstructions('Candidate Hired'), + extraFields: buildGreenhouseExtraFields('greenhouse_candidate_hired'), }), outputs: buildCandidateHiredOutputs(), diff --git a/apps/sim/triggers/greenhouse/candidate_rejected.ts b/apps/sim/triggers/greenhouse/candidate_rejected.ts index 119f24c6f60..52f10c038e4 100644 --- a/apps/sim/triggers/greenhouse/candidate_rejected.ts +++ b/apps/sim/triggers/greenhouse/candidate_rejected.ts @@ -2,6 +2,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { buildCandidateRejectedOutputs, + buildGreenhouseExtraFields, greenhouseSetupInstructions, greenhouseTriggerOptions, } from '@/triggers/greenhouse/utils' @@ -24,6 +25,7 @@ export const greenhouseCandidateRejectedTrigger: TriggerConfig = { triggerId: 'greenhouse_candidate_rejected', triggerOptions: greenhouseTriggerOptions, setupInstructions: greenhouseSetupInstructions('Candidate Rejected'), + extraFields: buildGreenhouseExtraFields('greenhouse_candidate_rejected'), }), outputs: buildCandidateRejectedOutputs(), diff --git a/apps/sim/triggers/greenhouse/candidate_stage_change.ts b/apps/sim/triggers/greenhouse/candidate_stage_change.ts index dffd7df656b..d9f9505f40d 100644 --- a/apps/sim/triggers/greenhouse/candidate_stage_change.ts +++ b/apps/sim/triggers/greenhouse/candidate_stage_change.ts @@ -2,6 +2,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { buildCandidateStageChangeOutputs, + buildGreenhouseExtraFields, greenhouseSetupInstructions, greenhouseTriggerOptions, } from '@/triggers/greenhouse/utils' @@ -24,6 +25,7 @@ export const greenhouseCandidateStageChangeTrigger: TriggerConfig = { triggerId: 'greenhouse_candidate_stage_change', triggerOptions: greenhouseTriggerOptions, setupInstructions: greenhouseSetupInstructions('Candidate Stage Change'), + extraFields: buildGreenhouseExtraFields('greenhouse_candidate_stage_change'), }), outputs: buildCandidateStageChangeOutputs(), diff --git a/apps/sim/triggers/greenhouse/job_created.ts b/apps/sim/triggers/greenhouse/job_created.ts index 17e8ff745b8..8cfefd33a12 100644 --- a/apps/sim/triggers/greenhouse/job_created.ts +++ b/apps/sim/triggers/greenhouse/job_created.ts @@ -1,6 +1,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { + buildGreenhouseExtraFields, buildJobCreatedOutputs, greenhouseSetupInstructions, greenhouseTriggerOptions, @@ -24,6 +25,7 @@ export const greenhouseJobCreatedTrigger: TriggerConfig = { triggerId: 'greenhouse_job_created', triggerOptions: greenhouseTriggerOptions, setupInstructions: greenhouseSetupInstructions('Job Created'), + extraFields: buildGreenhouseExtraFields('greenhouse_job_created'), }), outputs: buildJobCreatedOutputs(), diff --git a/apps/sim/triggers/greenhouse/job_updated.ts b/apps/sim/triggers/greenhouse/job_updated.ts index 8ca6cdb678e..c669ef22ec0 100644 --- a/apps/sim/triggers/greenhouse/job_updated.ts +++ b/apps/sim/triggers/greenhouse/job_updated.ts @@ -1,6 +1,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { + buildGreenhouseExtraFields, buildJobUpdatedOutputs, greenhouseSetupInstructions, greenhouseTriggerOptions, @@ -24,6 +25,7 @@ export const greenhouseJobUpdatedTrigger: TriggerConfig = { triggerId: 'greenhouse_job_updated', triggerOptions: greenhouseTriggerOptions, setupInstructions: greenhouseSetupInstructions('Job Updated'), + extraFields: buildGreenhouseExtraFields('greenhouse_job_updated'), }), outputs: buildJobUpdatedOutputs(), diff --git a/apps/sim/triggers/greenhouse/new_application.ts b/apps/sim/triggers/greenhouse/new_application.ts index f09a928865a..933cd624e14 100644 --- a/apps/sim/triggers/greenhouse/new_application.ts +++ b/apps/sim/triggers/greenhouse/new_application.ts @@ -1,6 +1,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { + buildGreenhouseExtraFields, buildNewApplicationOutputs, greenhouseSetupInstructions, greenhouseTriggerOptions, @@ -24,6 +25,7 @@ export const greenhouseNewApplicationTrigger: TriggerConfig = { triggerId: 'greenhouse_new_application', triggerOptions: greenhouseTriggerOptions, setupInstructions: greenhouseSetupInstructions('New Candidate Application'), + extraFields: buildGreenhouseExtraFields('greenhouse_new_application'), }), outputs: buildNewApplicationOutputs(), diff --git a/apps/sim/triggers/greenhouse/offer_created.ts b/apps/sim/triggers/greenhouse/offer_created.ts index 2de60d342a4..7567a9adb63 100644 --- a/apps/sim/triggers/greenhouse/offer_created.ts +++ b/apps/sim/triggers/greenhouse/offer_created.ts @@ -1,6 +1,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { + buildGreenhouseExtraFields, buildOfferCreatedOutputs, greenhouseSetupInstructions, greenhouseTriggerOptions, @@ -24,6 +25,7 @@ export const greenhouseOfferCreatedTrigger: TriggerConfig = { triggerId: 'greenhouse_offer_created', triggerOptions: greenhouseTriggerOptions, setupInstructions: greenhouseSetupInstructions('Offer Created'), + extraFields: buildGreenhouseExtraFields('greenhouse_offer_created'), }), outputs: buildOfferCreatedOutputs(), diff --git a/apps/sim/triggers/greenhouse/utils.ts b/apps/sim/triggers/greenhouse/utils.ts index 7d6169c053b..d5712f0d9a7 100644 --- a/apps/sim/triggers/greenhouse/utils.ts +++ b/apps/sim/triggers/greenhouse/utils.ts @@ -1,3 +1,4 @@ +import type { SubBlockConfig } from '@/blocks/types' import type { TriggerOutput } from '@/triggers/types' /** @@ -39,6 +40,25 @@ export function isGreenhouseEventMatch(triggerId: string, action: string): boole return action === expectedAction } +/** + * Builds extra fields for Greenhouse triggers. + * Includes an optional secret key for HMAC signature verification. + */ +export function buildGreenhouseExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'secretKey', + title: 'Secret Key (Optional)', + type: 'short-input', + placeholder: 'Enter the same secret key configured in Greenhouse', + description: 'Used to verify webhook signatures via HMAC-SHA256.', + password: true, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + /** * Generates HTML setup instructions for Greenhouse webhooks. * Webhooks are manually configured in the Greenhouse admin panel. @@ -221,7 +241,7 @@ export function buildCandidateRejectedOutputs(): Record { rejection_reason: { id: { type: 'number', description: 'Rejection reason ID' }, name: { type: 'string', description: 'Rejection reason name' }, - type: { + reason_type: { id: { type: 'number', description: 'Rejection reason type ID' }, name: { type: 'string', description: 'Rejection reason type name' }, }, @@ -256,6 +276,27 @@ export function buildOfferCreatedOutputs(): Record { } as Record } +/** + * Shared job payload shape used by both job_created and job_updated events. + */ +function buildJobPayload(): Record { + return { + id: { type: 'number', description: 'Job ID' }, + name: { type: 'string', description: 'Job title' }, + requisition_id: { type: 'string', description: 'Requisition ID' }, + status: { type: 'string', description: 'Job status (open, closed, draft)' }, + confidential: { type: 'boolean', description: 'Whether the job is confidential' }, + created_at: { type: 'string', description: 'When the job was created' }, + opened_at: { type: 'string', description: 'When the job was opened' }, + closed_at: { type: 'string', description: 'When the job was closed' }, + departments: { type: 'json', description: 'Associated departments' }, + offices: { type: 'json', description: 'Associated offices' }, + hiring_team: { type: 'json', description: 'Hiring team (managers, recruiters, etc.)' }, + openings: { type: 'json', description: 'Job openings' }, + custom_fields: { type: 'json', description: 'Custom field values' }, + } as Record +} + /** * Build outputs for job_created events. * Job data is nested under payload.job. @@ -263,23 +304,7 @@ export function buildOfferCreatedOutputs(): Record { export function buildJobCreatedOutputs(): Record { return { action: { type: 'string', description: 'The webhook event type (job_created)' }, - payload: { - job: { - id: { type: 'number', description: 'Job ID' }, - name: { type: 'string', description: 'Job title' }, - requisition_id: { type: 'string', description: 'Requisition ID' }, - status: { type: 'string', description: 'Job status (open, closed, draft)' }, - confidential: { type: 'boolean', description: 'Whether the job is confidential' }, - created_at: { type: 'string', description: 'When the job was created' }, - opened_at: { type: 'string', description: 'When the job was opened' }, - closed_at: { type: 'string', description: 'When the job was closed' }, - departments: { type: 'json', description: 'Associated departments' }, - offices: { type: 'json', description: 'Associated offices' }, - hiring_team: { type: 'json', description: 'Hiring team (managers, recruiters, etc.)' }, - openings: { type: 'json', description: 'Job openings' }, - custom_fields: { type: 'json', description: 'Custom field values' }, - }, - }, + payload: { job: buildJobPayload() }, } as Record } @@ -290,23 +315,7 @@ export function buildJobCreatedOutputs(): Record { export function buildJobUpdatedOutputs(): Record { return { action: { type: 'string', description: 'The webhook event type (job_updated)' }, - payload: { - job: { - id: { type: 'number', description: 'Job ID' }, - name: { type: 'string', description: 'Job title' }, - requisition_id: { type: 'string', description: 'Requisition ID' }, - status: { type: 'string', description: 'Job status (open, closed, draft)' }, - confidential: { type: 'boolean', description: 'Whether the job is confidential' }, - created_at: { type: 'string', description: 'When the job was created' }, - opened_at: { type: 'string', description: 'When the job was opened' }, - closed_at: { type: 'string', description: 'When the job was closed' }, - departments: { type: 'json', description: 'Associated departments' }, - offices: { type: 'json', description: 'Associated offices' }, - hiring_team: { type: 'json', description: 'Hiring team (managers, recruiters, etc.)' }, - openings: { type: 'json', description: 'Job openings' }, - custom_fields: { type: 'json', description: 'Custom field values' }, - }, - }, + payload: { job: buildJobPayload() }, } as Record } diff --git a/apps/sim/triggers/greenhouse/webhook.ts b/apps/sim/triggers/greenhouse/webhook.ts index 7cf39565a09..de436a89748 100644 --- a/apps/sim/triggers/greenhouse/webhook.ts +++ b/apps/sim/triggers/greenhouse/webhook.ts @@ -1,6 +1,7 @@ import { GreenhouseIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { + buildGreenhouseExtraFields, buildWebhookOutputs, greenhouseSetupInstructions, greenhouseTriggerOptions, @@ -24,6 +25,7 @@ export const greenhouseWebhookTrigger: TriggerConfig = { triggerId: 'greenhouse_webhook', triggerOptions: greenhouseTriggerOptions, setupInstructions: greenhouseSetupInstructions('All Events'), + extraFields: buildGreenhouseExtraFields('greenhouse_webhook'), }), outputs: buildWebhookOutputs(), From 8518fe34eece3694236d0cc4c2e545440fa59854 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 11:29:17 -0700 Subject: [PATCH 3/3] fix(triggers): align rejection_reason output with actual Greenhouse payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverted reason_type rename — instead flattened rejection_reason to JSON type since TriggerOutput's type?: string conflicts with nested type keys. Also hardened processOutputField to check typeof type === 'string' before treating an object as a leaf node, preventing this class of bug for future triggers. Co-Authored-By: Claude Opus 4.6 --- apps/sim/lib/workflows/triggers/trigger-utils.ts | 7 ++++++- apps/sim/triggers/greenhouse/utils.ts | 8 ++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/sim/lib/workflows/triggers/trigger-utils.ts b/apps/sim/lib/workflows/triggers/trigger-utils.ts index 276e28ce889..f1ee5ce7450 100644 --- a/apps/sim/lib/workflows/triggers/trigger-utils.ts +++ b/apps/sim/lib/workflows/triggers/trigger-utils.ts @@ -74,7 +74,12 @@ function processOutputField(key: string, field: unknown, depth = 0, maxDepth = 1 return null } - if (field && typeof field === 'object' && 'type' in field) { + if ( + field && + typeof field === 'object' && + 'type' in field && + typeof (field as Record).type === 'string' + ) { const typedField = field as { type: string; description?: string } return generateMockValue(typedField.type, typedField.description, key) } diff --git a/apps/sim/triggers/greenhouse/utils.ts b/apps/sim/triggers/greenhouse/utils.ts index d5712f0d9a7..15972379e03 100644 --- a/apps/sim/triggers/greenhouse/utils.ts +++ b/apps/sim/triggers/greenhouse/utils.ts @@ -239,12 +239,8 @@ export function buildCandidateRejectedOutputs(): Record { }, jobs: { type: 'json', description: 'Associated jobs (array)' }, rejection_reason: { - id: { type: 'number', description: 'Rejection reason ID' }, - name: { type: 'string', description: 'Rejection reason name' }, - reason_type: { - id: { type: 'number', description: 'Rejection reason type ID' }, - name: { type: 'string', description: 'Rejection reason type name' }, - }, + type: 'json', + description: 'Rejection reason object with id, name, and type fields', }, rejection_details: { type: 'json', description: 'Rejection details with custom fields' }, custom_fields: { type: 'json', description: 'Application custom fields' },