mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
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:
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
35
apps/sim/triggers/salesforce/case_status_changed.ts
Normal file
35
apps/sim/triggers/salesforce/case_status_changed.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
6
apps/sim/triggers/salesforce/index.ts
Normal file
6
apps/sim/triggers/salesforce/index.ts
Normal 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'
|
||||
35
apps/sim/triggers/salesforce/opportunity_stage_changed.ts
Normal file
35
apps/sim/triggers/salesforce/opportunity_stage_changed.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
40
apps/sim/triggers/salesforce/record_created.ts
Normal file
40
apps/sim/triggers/salesforce/record_created.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
37
apps/sim/triggers/salesforce/record_deleted.ts
Normal file
37
apps/sim/triggers/salesforce/record_deleted.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
37
apps/sim/triggers/salesforce/record_updated.ts
Normal file
37
apps/sim/triggers/salesforce/record_updated.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
154
apps/sim/triggers/salesforce/utils.ts
Normal file
154
apps/sim/triggers/salesforce/utils.ts
Normal 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' },
|
||||
}
|
||||
}
|
||||
37
apps/sim/triggers/salesforce/webhook.ts
Normal file
37
apps/sim/triggers/salesforce/webhook.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user