mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(triggers): add Greenhouse webhook triggers (#3985)
* 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. * 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 * fix(triggers): align rejection_reason output with actual Greenhouse payload 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<GreenhouseResponse> = {
|
||||
type: 'greenhouse',
|
||||
@@ -16,6 +17,20 @@ export const GreenhouseBlock: BlockConfig<GreenhouseResponse> = {
|
||||
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: {
|
||||
|
||||
80
apps/sim/lib/webhooks/providers/greenhouse.ts
Normal file
80
apps/sim/lib/webhooks/providers/greenhouse.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
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 <hexdigest>`
|
||||
*/
|
||||
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<FormatInputResult> {
|
||||
const b = body as Record<string, unknown>
|
||||
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<string, unknown>
|
||||
const action = b.action as string | undefined
|
||||
|
||||
if (triggerId && triggerId !== 'greenhouse_webhook') {
|
||||
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
|
||||
},
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { gmailHandler } from '@/lib/webhooks/providers/gmail'
|
||||
import { gongHandler } from '@/lib/webhooks/providers/gong'
|
||||
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 { intercomHandler } from '@/lib/webhooks/providers/intercom'
|
||||
@@ -54,6 +55,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
|
||||
google_forms: googleFormsHandler,
|
||||
fathom: fathomHandler,
|
||||
grain: grainHandler,
|
||||
greenhouse: greenhouseHandler,
|
||||
hubspot: hubspotHandler,
|
||||
imap: imapHandler,
|
||||
intercom: intercomHandler,
|
||||
|
||||
@@ -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<string, unknown>).type === 'string'
|
||||
) {
|
||||
const typedField = field as { type: string; description?: string }
|
||||
return generateMockValue(typedField.type, typedField.description, key)
|
||||
}
|
||||
|
||||
41
apps/sim/triggers/greenhouse/candidate_hired.ts
Normal file
41
apps/sim/triggers/greenhouse/candidate_hired.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildCandidateHiredOutputs,
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_candidate_hired'),
|
||||
}),
|
||||
|
||||
outputs: buildCandidateHiredOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
39
apps/sim/triggers/greenhouse/candidate_rejected.ts
Normal file
39
apps/sim/triggers/greenhouse/candidate_rejected.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildCandidateRejectedOutputs,
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_candidate_rejected'),
|
||||
}),
|
||||
|
||||
outputs: buildCandidateRejectedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
39
apps/sim/triggers/greenhouse/candidate_stage_change.ts
Normal file
39
apps/sim/triggers/greenhouse/candidate_stage_change.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildCandidateStageChangeOutputs,
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_candidate_stage_change'),
|
||||
}),
|
||||
|
||||
outputs: buildCandidateStageChangeOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
8
apps/sim/triggers/greenhouse/index.ts
Normal file
8
apps/sim/triggers/greenhouse/index.ts
Normal file
@@ -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'
|
||||
39
apps/sim/triggers/greenhouse/job_created.ts
Normal file
39
apps/sim/triggers/greenhouse/job_created.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_job_created'),
|
||||
}),
|
||||
|
||||
outputs: buildJobCreatedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
39
apps/sim/triggers/greenhouse/job_updated.ts
Normal file
39
apps/sim/triggers/greenhouse/job_updated.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_job_updated'),
|
||||
}),
|
||||
|
||||
outputs: buildJobUpdatedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
39
apps/sim/triggers/greenhouse/new_application.ts
Normal file
39
apps/sim/triggers/greenhouse/new_application.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_new_application'),
|
||||
}),
|
||||
|
||||
outputs: buildNewApplicationOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
39
apps/sim/triggers/greenhouse/offer_created.ts
Normal file
39
apps/sim/triggers/greenhouse/offer_created.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_offer_created'),
|
||||
}),
|
||||
|
||||
outputs: buildOfferCreatedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
326
apps/sim/triggers/greenhouse/utils.ts
Normal file
326
apps/sim/triggers/greenhouse/utils.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
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<string, string> = {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function greenhouseSetupInstructions(eventType: string): string {
|
||||
const instructions = [
|
||||
'Copy the <strong>Webhook URL</strong> above.',
|
||||
'In Greenhouse, go to <strong>Configure > Dev Center > Webhooks</strong>.',
|
||||
'Click <strong>Create New Webhook</strong>.',
|
||||
'Paste the Webhook URL into the <strong>Endpoint URL</strong> field.',
|
||||
'Enter a <strong>Secret Key</strong> for signature verification (optional).',
|
||||
`Under <strong>When</strong>, select the <strong>${eventType}</strong> event.`,
|
||||
'Click <strong>Create Webhook</strong> to save.',
|
||||
'Click "Save" above to activate your trigger.',
|
||||
]
|
||||
|
||||
return instructions
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.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<string, TriggerOutput> {
|
||||
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<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for new_candidate_application events.
|
||||
* Candidate is nested inside application: payload.application.candidate
|
||||
*/
|
||||
export function buildNewApplicationOutputs(): Record<string, TriggerOutput> {
|
||||
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<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for candidate_stage_change events.
|
||||
* Candidate is nested inside application: payload.application.candidate
|
||||
*/
|
||||
export function buildCandidateStageChangeOutputs(): Record<string, TriggerOutput> {
|
||||
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<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for reject_candidate events.
|
||||
* Candidate is nested inside application: payload.application.candidate
|
||||
*/
|
||||
export function buildCandidateRejectedOutputs(): Record<string, TriggerOutput> {
|
||||
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: {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for offer_created events.
|
||||
* Offer payload is flat under payload (not nested under payload.offer).
|
||||
*/
|
||||
export function buildOfferCreatedOutputs(): Record<string, TriggerOutput> {
|
||||
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<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared job payload shape used by both job_created and job_updated events.
|
||||
*/
|
||||
function buildJobPayload(): Record<string, TriggerOutput> {
|
||||
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<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for job_created events.
|
||||
* Job data is nested under payload.job.
|
||||
*/
|
||||
export function buildJobCreatedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
action: { type: 'string', description: 'The webhook event type (job_created)' },
|
||||
payload: { job: buildJobPayload() },
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for job_updated events.
|
||||
* Same structure as job_created.
|
||||
*/
|
||||
export function buildJobUpdatedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
action: { type: 'string', description: 'The webhook event type (job_updated)' },
|
||||
payload: { job: buildJobPayload() },
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for generic webhook (all events).
|
||||
*/
|
||||
export function buildWebhookOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
action: { type: 'string', description: 'The webhook event type' },
|
||||
payload: { type: 'json', description: 'Full event payload' },
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
39
apps/sim/triggers/greenhouse/webhook.ts
Normal file
39
apps/sim/triggers/greenhouse/webhook.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GreenhouseIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildGreenhouseExtraFields,
|
||||
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'),
|
||||
extraFields: buildGreenhouseExtraFields('greenhouse_webhook'),
|
||||
}),
|
||||
|
||||
outputs: buildWebhookOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -101,6 +101,16 @@ import {
|
||||
grainStoryCreatedTrigger,
|
||||
grainWebhookTrigger,
|
||||
} from '@/triggers/grain'
|
||||
import {
|
||||
greenhouseCandidateHiredTrigger,
|
||||
greenhouseCandidateRejectedTrigger,
|
||||
greenhouseCandidateStageChangeTrigger,
|
||||
greenhouseJobCreatedTrigger,
|
||||
greenhouseJobUpdatedTrigger,
|
||||
greenhouseNewApplicationTrigger,
|
||||
greenhouseOfferCreatedTrigger,
|
||||
greenhouseWebhookTrigger,
|
||||
} from '@/triggers/greenhouse'
|
||||
import {
|
||||
hubspotCompanyCreatedTrigger,
|
||||
hubspotCompanyDeletedTrigger,
|
||||
@@ -274,6 +284,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,
|
||||
|
||||
Reference in New Issue
Block a user