feat(triggers): add Salesforce webhook triggers (#3982)

* feat(triggers): add Salesforce webhook triggers

* fix(triggers): address PR review — remove non-TSDoc comment, fix generic webhook instructions
This commit is contained in:
Waleed
2026-04-06 11:06:36 -07:00
committed by GitHub
parent 5ca66c381b
commit 925be3d635
10 changed files with 413 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { SalesforceResponse } from '@/tools/salesforce/types'
import { getTrigger } from '@/triggers'
export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
type: 'salesforce',
@@ -17,6 +18,17 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
tags: ['sales-engagement', 'customer-support'],
bgColor: '#E0E0E0',
icon: SalesforceIcon,
triggers: {
enabled: true,
available: [
'salesforce_record_created',
'salesforce_record_updated',
'salesforce_record_deleted',
'salesforce_opportunity_stage_changed',
'salesforce_case_status_changed',
'salesforce_webhook',
],
},
subBlocks: [
{
id: 'operation',
@@ -511,6 +523,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
],
},
},
...getTrigger('salesforce_record_created').subBlocks,
...getTrigger('salesforce_record_updated').subBlocks,
...getTrigger('salesforce_record_deleted').subBlocks,
...getTrigger('salesforce_opportunity_stage_changed').subBlocks,
...getTrigger('salesforce_case_status_changed').subBlocks,
...getTrigger('salesforce_webhook').subBlocks,
],
tools: {
access: [

View File

@@ -163,6 +163,14 @@ import {
} from '@/triggers/microsoftteams'
import { outlookPollingTrigger } from '@/triggers/outlook'
import { rssPollingTrigger } from '@/triggers/rss'
import {
salesforceCaseStatusChangedTrigger,
salesforceOpportunityStageChangedTrigger,
salesforceRecordCreatedTrigger,
salesforceRecordDeletedTrigger,
salesforceRecordUpdatedTrigger,
salesforceWebhookTrigger,
} from '@/triggers/salesforce'
import { slackWebhookTrigger } from '@/triggers/slack'
import { stripeWebhookTrigger } from '@/triggers/stripe'
import { telegramWebhookTrigger } from '@/triggers/telegram'
@@ -299,6 +307,12 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
microsoftteams_chat_subscription: microsoftTeamsChatSubscriptionTrigger,
outlook_poller: outlookPollingTrigger,
rss_poller: rssPollingTrigger,
salesforce_record_created: salesforceRecordCreatedTrigger,
salesforce_record_updated: salesforceRecordUpdatedTrigger,
salesforce_record_deleted: salesforceRecordDeletedTrigger,
salesforce_opportunity_stage_changed: salesforceOpportunityStageChangedTrigger,
salesforce_case_status_changed: salesforceCaseStatusChangedTrigger,
salesforce_webhook: salesforceWebhookTrigger,
stripe_webhook: stripeWebhookTrigger,
telegram_webhook: telegramWebhookTrigger,
typeform_webhook: typeformWebhookTrigger,

View File

@@ -0,0 +1,35 @@
import { SalesforceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSalesforceCaseStatusOutputs,
salesforceSetupInstructions,
salesforceTriggerOptions,
} from '@/triggers/salesforce/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Salesforce Case Status Changed Trigger
*/
export const salesforceCaseStatusChangedTrigger: TriggerConfig = {
id: 'salesforce_case_status_changed',
name: 'Salesforce Case Status Changed',
provider: 'salesforce',
description: 'Trigger workflow when a case status changes',
version: '1.0.0',
icon: SalesforceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'salesforce_case_status_changed',
triggerOptions: salesforceTriggerOptions,
setupInstructions: salesforceSetupInstructions('Case Status Changed'),
}),
outputs: buildSalesforceCaseStatusOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,6 @@
export { salesforceCaseStatusChangedTrigger } from './case_status_changed'
export { salesforceOpportunityStageChangedTrigger } from './opportunity_stage_changed'
export { salesforceRecordCreatedTrigger } from './record_created'
export { salesforceRecordDeletedTrigger } from './record_deleted'
export { salesforceRecordUpdatedTrigger } from './record_updated'
export { salesforceWebhookTrigger } from './webhook'

View File

@@ -0,0 +1,35 @@
import { SalesforceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSalesforceOpportunityStageOutputs,
salesforceSetupInstructions,
salesforceTriggerOptions,
} from '@/triggers/salesforce/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Salesforce Opportunity Stage Changed Trigger
*/
export const salesforceOpportunityStageChangedTrigger: TriggerConfig = {
id: 'salesforce_opportunity_stage_changed',
name: 'Salesforce Opportunity Stage Changed',
provider: 'salesforce',
description: 'Trigger workflow when an opportunity stage changes',
version: '1.0.0',
icon: SalesforceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'salesforce_opportunity_stage_changed',
triggerOptions: salesforceTriggerOptions,
setupInstructions: salesforceSetupInstructions('Opportunity Stage Changed'),
}),
outputs: buildSalesforceOpportunityStageOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,40 @@
import { SalesforceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSalesforceExtraFields,
buildSalesforceRecordOutputs,
salesforceSetupInstructions,
salesforceTriggerOptions,
} from '@/triggers/salesforce/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Salesforce Record Created Trigger
*
* PRIMARY trigger — includes the dropdown for selecting trigger type.
*/
export const salesforceRecordCreatedTrigger: TriggerConfig = {
id: 'salesforce_record_created',
name: 'Salesforce Record Created',
provider: 'salesforce',
description: 'Trigger workflow when a Salesforce record is created',
version: '1.0.0',
icon: SalesforceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'salesforce_record_created',
triggerOptions: salesforceTriggerOptions,
includeDropdown: true,
setupInstructions: salesforceSetupInstructions('Record Created'),
extraFields: buildSalesforceExtraFields('salesforce_record_created'),
}),
outputs: buildSalesforceRecordOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,37 @@
import { SalesforceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSalesforceExtraFields,
buildSalesforceRecordOutputs,
salesforceSetupInstructions,
salesforceTriggerOptions,
} from '@/triggers/salesforce/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Salesforce Record Deleted Trigger
*/
export const salesforceRecordDeletedTrigger: TriggerConfig = {
id: 'salesforce_record_deleted',
name: 'Salesforce Record Deleted',
provider: 'salesforce',
description: 'Trigger workflow when a Salesforce record is deleted',
version: '1.0.0',
icon: SalesforceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'salesforce_record_deleted',
triggerOptions: salesforceTriggerOptions,
setupInstructions: salesforceSetupInstructions('Record Deleted'),
extraFields: buildSalesforceExtraFields('salesforce_record_deleted'),
}),
outputs: buildSalesforceRecordOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,37 @@
import { SalesforceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSalesforceExtraFields,
buildSalesforceRecordOutputs,
salesforceSetupInstructions,
salesforceTriggerOptions,
} from '@/triggers/salesforce/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Salesforce Record Updated Trigger
*/
export const salesforceRecordUpdatedTrigger: TriggerConfig = {
id: 'salesforce_record_updated',
name: 'Salesforce Record Updated',
provider: 'salesforce',
description: 'Trigger workflow when a Salesforce record is updated',
version: '1.0.0',
icon: SalesforceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'salesforce_record_updated',
triggerOptions: salesforceTriggerOptions,
setupInstructions: salesforceSetupInstructions('Record Updated'),
extraFields: buildSalesforceExtraFields('salesforce_record_updated'),
}),
outputs: buildSalesforceRecordOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,154 @@
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
/**
* Dropdown options for the Salesforce trigger type selector.
*/
export const salesforceTriggerOptions = [
{ label: 'Record Created', id: 'salesforce_record_created' },
{ label: 'Record Updated', id: 'salesforce_record_updated' },
{ label: 'Record Deleted', id: 'salesforce_record_deleted' },
{ label: 'Opportunity Stage Changed', id: 'salesforce_opportunity_stage_changed' },
{ label: 'Case Status Changed', id: 'salesforce_case_status_changed' },
{ label: 'Generic Webhook (All Events)', id: 'salesforce_webhook' },
]
/**
* Generates HTML setup instructions for the Salesforce trigger.
* Salesforce has no native webhook API — users must configure
* Flow HTTP Callouts or Outbound Messages manually.
*/
export function salesforceSetupInstructions(eventType: string): string {
const isGeneric = eventType === 'All Events'
const instructions = isGeneric
? [
'Copy the <strong>Webhook URL</strong> above.',
'In Salesforce, go to <strong>Setup → Flows</strong> and click <strong>New Flow</strong>.',
'Select <strong>Record-Triggered Flow</strong> and choose the object(s) you want to monitor.',
'Add an <strong>HTTP Callout</strong> action — set the method to <strong>POST</strong> and paste the webhook URL.',
'In the request body, include the record fields you want sent as <strong>JSON</strong> (e.g., Id, Name, and any relevant fields).',
'Repeat for each object type you want to send events for.',
'Save and <strong>Activate</strong> the Flow(s).',
'Click <strong>"Save"</strong> above to activate your trigger.',
]
: [
'Copy the <strong>Webhook URL</strong> above.',
'In Salesforce, go to <strong>Setup → Flows</strong> and click <strong>New Flow</strong>.',
`Select <strong>Record-Triggered Flow</strong> and choose the object and <strong>${eventType}</strong> trigger condition.`,
'Add an <strong>HTTP Callout</strong> action — set the method to <strong>POST</strong> and paste the webhook URL.',
'In the request body, include the record fields you want sent as <strong>JSON</strong> (e.g., Id, Name, and any relevant fields).',
'Save and <strong>Activate</strong> the Flow.',
'Click <strong>"Save"</strong> above to activate your trigger.',
'<em>Alternative: You can also use <strong>Setup → Outbound Messages</strong> with a Workflow Rule, but this sends SOAP/XML instead of JSON.</em>',
]
return instructions
.map(
(instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
/**
* Extra fields for Salesforce triggers (object type filter).
*/
export function buildSalesforceExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'objectType',
title: 'Object Type (Optional)',
type: 'short-input',
placeholder: 'e.g., Account, Contact, Lead, Opportunity',
description: 'Optionally filter to a specific Salesforce object type',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
/**
* Outputs for record lifecycle events (created, updated, deleted).
*/
export function buildSalesforceRecordOutputs(): Record<string, TriggerOutput> {
return {
eventType: {
type: 'string',
description: 'The type of event (e.g., created, updated, deleted)',
},
objectType: {
type: 'string',
description: 'Salesforce object type (e.g., Account, Contact, Lead)',
},
recordId: { type: 'string', description: 'ID of the affected record' },
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
record: {
Id: { type: 'string', description: 'Record ID' },
Name: { type: 'string', description: 'Record name' },
CreatedDate: { type: 'string', description: 'Record creation date' },
LastModifiedDate: { type: 'string', description: 'Last modification date' },
},
changedFields: { type: 'json', description: 'Fields that were changed (for update events)' },
payload: { type: 'json', description: 'Full webhook payload' },
}
}
/**
* Outputs for opportunity stage change events.
*/
export function buildSalesforceOpportunityStageOutputs(): Record<string, TriggerOutput> {
return {
eventType: { type: 'string', description: 'The type of event' },
objectType: { type: 'string', description: 'Salesforce object type (Opportunity)' },
recordId: { type: 'string', description: 'Opportunity ID' },
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
record: {
Id: { type: 'string', description: 'Opportunity ID' },
Name: { type: 'string', description: 'Opportunity name' },
StageName: { type: 'string', description: 'Current stage name' },
Amount: { type: 'string', description: 'Deal amount' },
CloseDate: { type: 'string', description: 'Expected close date' },
Probability: { type: 'string', description: 'Win probability' },
},
previousStage: { type: 'string', description: 'Previous stage name' },
newStage: { type: 'string', description: 'New stage name' },
payload: { type: 'json', description: 'Full webhook payload' },
}
}
/**
* Outputs for case status change events.
*/
export function buildSalesforceCaseStatusOutputs(): Record<string, TriggerOutput> {
return {
eventType: { type: 'string', description: 'The type of event' },
objectType: { type: 'string', description: 'Salesforce object type (Case)' },
recordId: { type: 'string', description: 'Case ID' },
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
record: {
Id: { type: 'string', description: 'Case ID' },
Subject: { type: 'string', description: 'Case subject' },
Status: { type: 'string', description: 'Current case status' },
Priority: { type: 'string', description: 'Case priority' },
CaseNumber: { type: 'string', description: 'Case number' },
},
previousStatus: { type: 'string', description: 'Previous case status' },
newStatus: { type: 'string', description: 'New case status' },
payload: { type: 'json', description: 'Full webhook payload' },
}
}
/**
* Outputs for the generic webhook trigger.
*/
export function buildSalesforceWebhookOutputs(): Record<string, TriggerOutput> {
return {
eventType: { type: 'string', description: 'The type of event' },
objectType: { type: 'string', description: 'Salesforce object type' },
recordId: { type: 'string', description: 'ID of the affected record' },
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
record: { type: 'json', description: 'Full record data' },
payload: { type: 'json', description: 'Full webhook payload' },
}
}

View File

@@ -0,0 +1,37 @@
import { SalesforceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildSalesforceWebhookOutputs,
salesforceSetupInstructions,
salesforceTriggerOptions,
} from '@/triggers/salesforce/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Salesforce Generic Webhook Trigger
*
* Receives all Salesforce events via a single webhook endpoint.
*/
export const salesforceWebhookTrigger: TriggerConfig = {
id: 'salesforce_webhook',
name: 'Salesforce Webhook (All Events)',
provider: 'salesforce',
description: 'Trigger workflow on any Salesforce webhook event',
version: '1.0.0',
icon: SalesforceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'salesforce_webhook',
triggerOptions: salesforceTriggerOptions,
setupInstructions: salesforceSetupInstructions('All Events'),
}),
outputs: buildSalesforceWebhookOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}