Skip to content

Commit 7ea0693

Browse files
waleedlatif1claude
andauthored
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>
1 parent 590f376 commit 7ea0693

File tree

15 files changed

+780
-1
lines changed

15 files changed

+780
-1
lines changed

apps/sim/blocks/blocks/greenhouse.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { GreenhouseIcon } from '@/components/icons'
22
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
33
import type { GreenhouseResponse } from '@/tools/greenhouse/types'
4+
import { getTrigger } from '@/triggers'
45

56
export const GreenhouseBlock: BlockConfig<GreenhouseResponse> = {
67
type: 'greenhouse',
@@ -16,6 +17,20 @@ export const GreenhouseBlock: BlockConfig<GreenhouseResponse> = {
1617
icon: GreenhouseIcon,
1718
authMode: AuthMode.ApiKey,
1819

20+
triggers: {
21+
enabled: true,
22+
available: [
23+
'greenhouse_candidate_hired',
24+
'greenhouse_new_application',
25+
'greenhouse_candidate_stage_change',
26+
'greenhouse_candidate_rejected',
27+
'greenhouse_offer_created',
28+
'greenhouse_job_created',
29+
'greenhouse_job_updated',
30+
'greenhouse_webhook',
31+
],
32+
},
33+
1934
subBlocks: [
2035
{
2136
id: 'operation',
@@ -291,6 +306,17 @@ Return ONLY the ISO 8601 timestamp - no explanations, no extra text.`,
291306
required: true,
292307
password: true,
293308
},
309+
310+
// ── Trigger subBlocks ──
311+
312+
...getTrigger('greenhouse_candidate_hired').subBlocks,
313+
...getTrigger('greenhouse_new_application').subBlocks,
314+
...getTrigger('greenhouse_candidate_stage_change').subBlocks,
315+
...getTrigger('greenhouse_candidate_rejected').subBlocks,
316+
...getTrigger('greenhouse_offer_created').subBlocks,
317+
...getTrigger('greenhouse_job_created').subBlocks,
318+
...getTrigger('greenhouse_job_updated').subBlocks,
319+
...getTrigger('greenhouse_webhook').subBlocks,
294320
],
295321

296322
tools: {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import crypto from 'crypto'
2+
import { createLogger } from '@sim/logger'
3+
import { NextResponse } from 'next/server'
4+
import { safeCompare } from '@/lib/core/security/encryption'
5+
import type {
6+
EventMatchContext,
7+
FormatInputContext,
8+
FormatInputResult,
9+
WebhookProviderHandler,
10+
} from '@/lib/webhooks/providers/types'
11+
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
12+
import { isGreenhouseEventMatch } from '@/triggers/greenhouse/utils'
13+
14+
const logger = createLogger('WebhookProvider:Greenhouse')
15+
16+
/**
17+
* Validates the Greenhouse HMAC-SHA256 signature.
18+
* Greenhouse sends: `Signature: sha256 <hexdigest>`
19+
*/
20+
function validateGreenhouseSignature(secretKey: string, signature: string, body: string): boolean {
21+
try {
22+
if (!secretKey || !signature || !body) {
23+
return false
24+
}
25+
const prefix = 'sha256 '
26+
if (!signature.startsWith(prefix)) {
27+
return false
28+
}
29+
const providedDigest = signature.substring(prefix.length)
30+
const computedDigest = crypto.createHmac('sha256', secretKey).update(body, 'utf8').digest('hex')
31+
return safeCompare(computedDigest, providedDigest)
32+
} catch {
33+
logger.error('Error validating Greenhouse signature')
34+
return false
35+
}
36+
}
37+
38+
export const greenhouseHandler: WebhookProviderHandler = {
39+
verifyAuth: createHmacVerifier({
40+
configKey: 'secretKey',
41+
headerName: 'signature',
42+
validateFn: validateGreenhouseSignature,
43+
providerLabel: 'Greenhouse',
44+
}),
45+
46+
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
47+
const b = body as Record<string, unknown>
48+
return {
49+
input: {
50+
action: b.action,
51+
payload: b.payload || {},
52+
},
53+
}
54+
},
55+
56+
async matchEvent({ webhook, body, requestId, providerConfig }: EventMatchContext) {
57+
const triggerId = providerConfig.triggerId as string | undefined
58+
const b = body as Record<string, unknown>
59+
const action = b.action as string | undefined
60+
61+
if (triggerId && triggerId !== 'greenhouse_webhook') {
62+
if (!isGreenhouseEventMatch(triggerId, action || '')) {
63+
logger.debug(
64+
`[${requestId}] Greenhouse event mismatch for trigger ${triggerId}. Action: ${action}. Skipping execution.`,
65+
{
66+
webhookId: webhook.id,
67+
triggerId,
68+
receivedAction: action,
69+
}
70+
)
71+
72+
return NextResponse.json({
73+
message: 'Event type does not match trigger configuration. Ignoring.',
74+
})
75+
}
76+
}
77+
78+
return true
79+
},
80+
}

apps/sim/lib/webhooks/providers/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { gmailHandler } from '@/lib/webhooks/providers/gmail'
1515
import { gongHandler } from '@/lib/webhooks/providers/gong'
1616
import { googleFormsHandler } from '@/lib/webhooks/providers/google-forms'
1717
import { grainHandler } from '@/lib/webhooks/providers/grain'
18+
import { greenhouseHandler } from '@/lib/webhooks/providers/greenhouse'
1819
import { hubspotHandler } from '@/lib/webhooks/providers/hubspot'
1920
import { imapHandler } from '@/lib/webhooks/providers/imap'
2021
import { intercomHandler } from '@/lib/webhooks/providers/intercom'
@@ -54,6 +55,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
5455
google_forms: googleFormsHandler,
5556
fathom: fathomHandler,
5657
grain: grainHandler,
58+
greenhouse: greenhouseHandler,
5759
hubspot: hubspotHandler,
5860
imap: imapHandler,
5961
intercom: intercomHandler,

apps/sim/lib/workflows/triggers/trigger-utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ function processOutputField(key: string, field: unknown, depth = 0, maxDepth = 1
7474
return null
7575
}
7676

77-
if (field && typeof field === 'object' && 'type' in field) {
77+
if (
78+
field &&
79+
typeof field === 'object' &&
80+
'type' in field &&
81+
typeof (field as Record<string, unknown>).type === 'string'
82+
) {
7883
const typedField = field as { type: string; description?: string }
7984
return generateMockValue(typedField.type, typedField.description, key)
8085
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { GreenhouseIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildCandidateHiredOutputs,
5+
buildGreenhouseExtraFields,
6+
greenhouseSetupInstructions,
7+
greenhouseTriggerOptions,
8+
} from '@/triggers/greenhouse/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Greenhouse Candidate Hired Trigger
13+
*
14+
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
15+
* Fires when a candidate is marked as hired in Greenhouse.
16+
*/
17+
export const greenhouseCandidateHiredTrigger: TriggerConfig = {
18+
id: 'greenhouse_candidate_hired',
19+
name: 'Greenhouse Candidate Hired',
20+
provider: 'greenhouse',
21+
description: 'Trigger workflow when a candidate is hired',
22+
version: '1.0.0',
23+
icon: GreenhouseIcon,
24+
25+
subBlocks: buildTriggerSubBlocks({
26+
triggerId: 'greenhouse_candidate_hired',
27+
triggerOptions: greenhouseTriggerOptions,
28+
includeDropdown: true,
29+
setupInstructions: greenhouseSetupInstructions('Candidate Hired'),
30+
extraFields: buildGreenhouseExtraFields('greenhouse_candidate_hired'),
31+
}),
32+
33+
outputs: buildCandidateHiredOutputs(),
34+
35+
webhook: {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
},
40+
},
41+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { GreenhouseIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildCandidateRejectedOutputs,
5+
buildGreenhouseExtraFields,
6+
greenhouseSetupInstructions,
7+
greenhouseTriggerOptions,
8+
} from '@/triggers/greenhouse/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Greenhouse Candidate Rejected Trigger
13+
*
14+
* Fires when a candidate is rejected from a position.
15+
*/
16+
export const greenhouseCandidateRejectedTrigger: TriggerConfig = {
17+
id: 'greenhouse_candidate_rejected',
18+
name: 'Greenhouse Candidate Rejected',
19+
provider: 'greenhouse',
20+
description: 'Trigger workflow when a candidate is rejected',
21+
version: '1.0.0',
22+
icon: GreenhouseIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'greenhouse_candidate_rejected',
26+
triggerOptions: greenhouseTriggerOptions,
27+
setupInstructions: greenhouseSetupInstructions('Candidate Rejected'),
28+
extraFields: buildGreenhouseExtraFields('greenhouse_candidate_rejected'),
29+
}),
30+
31+
outputs: buildCandidateRejectedOutputs(),
32+
33+
webhook: {
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
},
38+
},
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { GreenhouseIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildCandidateStageChangeOutputs,
5+
buildGreenhouseExtraFields,
6+
greenhouseSetupInstructions,
7+
greenhouseTriggerOptions,
8+
} from '@/triggers/greenhouse/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Greenhouse Candidate Stage Change Trigger
13+
*
14+
* Fires when a candidate moves to a different interview stage.
15+
*/
16+
export const greenhouseCandidateStageChangeTrigger: TriggerConfig = {
17+
id: 'greenhouse_candidate_stage_change',
18+
name: 'Greenhouse Candidate Stage Change',
19+
provider: 'greenhouse',
20+
description: 'Trigger workflow when a candidate changes interview stages',
21+
version: '1.0.0',
22+
icon: GreenhouseIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'greenhouse_candidate_stage_change',
26+
triggerOptions: greenhouseTriggerOptions,
27+
setupInstructions: greenhouseSetupInstructions('Candidate Stage Change'),
28+
extraFields: buildGreenhouseExtraFields('greenhouse_candidate_stage_change'),
29+
}),
30+
31+
outputs: buildCandidateStageChangeOutputs(),
32+
33+
webhook: {
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
},
38+
},
39+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export { greenhouseCandidateHiredTrigger } from './candidate_hired'
2+
export { greenhouseCandidateRejectedTrigger } from './candidate_rejected'
3+
export { greenhouseCandidateStageChangeTrigger } from './candidate_stage_change'
4+
export { greenhouseJobCreatedTrigger } from './job_created'
5+
export { greenhouseJobUpdatedTrigger } from './job_updated'
6+
export { greenhouseNewApplicationTrigger } from './new_application'
7+
export { greenhouseOfferCreatedTrigger } from './offer_created'
8+
export { greenhouseWebhookTrigger } from './webhook'
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { GreenhouseIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildGreenhouseExtraFields,
5+
buildJobCreatedOutputs,
6+
greenhouseSetupInstructions,
7+
greenhouseTriggerOptions,
8+
} from '@/triggers/greenhouse/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Greenhouse Job Created Trigger
13+
*
14+
* Fires when a new job posting is created.
15+
*/
16+
export const greenhouseJobCreatedTrigger: TriggerConfig = {
17+
id: 'greenhouse_job_created',
18+
name: 'Greenhouse Job Created',
19+
provider: 'greenhouse',
20+
description: 'Trigger workflow when a new job is created',
21+
version: '1.0.0',
22+
icon: GreenhouseIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'greenhouse_job_created',
26+
triggerOptions: greenhouseTriggerOptions,
27+
setupInstructions: greenhouseSetupInstructions('Job Created'),
28+
extraFields: buildGreenhouseExtraFields('greenhouse_job_created'),
29+
}),
30+
31+
outputs: buildJobCreatedOutputs(),
32+
33+
webhook: {
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
},
38+
},
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { GreenhouseIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildGreenhouseExtraFields,
5+
buildJobUpdatedOutputs,
6+
greenhouseSetupInstructions,
7+
greenhouseTriggerOptions,
8+
} from '@/triggers/greenhouse/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Greenhouse Job Updated Trigger
13+
*
14+
* Fires when a job posting is updated.
15+
*/
16+
export const greenhouseJobUpdatedTrigger: TriggerConfig = {
17+
id: 'greenhouse_job_updated',
18+
name: 'Greenhouse Job Updated',
19+
provider: 'greenhouse',
20+
description: 'Trigger workflow when a job is updated',
21+
version: '1.0.0',
22+
icon: GreenhouseIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'greenhouse_job_updated',
26+
triggerOptions: greenhouseTriggerOptions,
27+
setupInstructions: greenhouseSetupInstructions('Job Updated'),
28+
extraFields: buildGreenhouseExtraFields('greenhouse_job_updated'),
29+
}),
30+
31+
outputs: buildJobUpdatedOutputs(),
32+
33+
webhook: {
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
},
38+
},
39+
}

0 commit comments

Comments
 (0)