diff --git a/apps/sim/blocks/blocks/salesforce.ts b/apps/sim/blocks/blocks/salesforce.ts index 53a9d67adae..53302520ca9 100644 --- a/apps/sim/blocks/blocks/salesforce.ts +++ b/apps/sim/blocks/blocks/salesforce.ts @@ -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 = { type: 'salesforce', @@ -17,6 +18,17 @@ export const SalesforceBlock: BlockConfig = { 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: [ diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 4390bfeefff..9671a62c9f7 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -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, diff --git a/apps/sim/triggers/salesforce/case_status_changed.ts b/apps/sim/triggers/salesforce/case_status_changed.ts new file mode 100644 index 00000000000..a3ad5802112 --- /dev/null +++ b/apps/sim/triggers/salesforce/case_status_changed.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/salesforce/index.ts b/apps/sim/triggers/salesforce/index.ts new file mode 100644 index 00000000000..93d02ffb9f9 --- /dev/null +++ b/apps/sim/triggers/salesforce/index.ts @@ -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' diff --git a/apps/sim/triggers/salesforce/opportunity_stage_changed.ts b/apps/sim/triggers/salesforce/opportunity_stage_changed.ts new file mode 100644 index 00000000000..43d72a972c3 --- /dev/null +++ b/apps/sim/triggers/salesforce/opportunity_stage_changed.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/salesforce/record_created.ts b/apps/sim/triggers/salesforce/record_created.ts new file mode 100644 index 00000000000..a1d3adf4224 --- /dev/null +++ b/apps/sim/triggers/salesforce/record_created.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/salesforce/record_deleted.ts b/apps/sim/triggers/salesforce/record_deleted.ts new file mode 100644 index 00000000000..72275317ec1 --- /dev/null +++ b/apps/sim/triggers/salesforce/record_deleted.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/salesforce/record_updated.ts b/apps/sim/triggers/salesforce/record_updated.ts new file mode 100644 index 00000000000..aac05c02a1c --- /dev/null +++ b/apps/sim/triggers/salesforce/record_updated.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/salesforce/utils.ts b/apps/sim/triggers/salesforce/utils.ts new file mode 100644 index 00000000000..a2c1db4b715 --- /dev/null +++ b/apps/sim/triggers/salesforce/utils.ts @@ -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 Webhook URL above.', + 'In Salesforce, go to Setup → Flows and click New Flow.', + 'Select Record-Triggered Flow and choose the object(s) you want to monitor.', + 'Add an HTTP Callout action — set the method to POST and paste the webhook URL.', + 'In the request body, include the record fields you want sent as JSON (e.g., Id, Name, and any relevant fields).', + 'Repeat for each object type you want to send events for.', + 'Save and Activate the Flow(s).', + 'Click "Save" above to activate your trigger.', + ] + : [ + 'Copy the Webhook URL above.', + 'In Salesforce, go to Setup → Flows and click New Flow.', + `Select Record-Triggered Flow and choose the object and ${eventType} trigger condition.`, + 'Add an HTTP Callout action — set the method to POST and paste the webhook URL.', + 'In the request body, include the record fields you want sent as JSON (e.g., Id, Name, and any relevant fields).', + 'Save and Activate the Flow.', + 'Click "Save" above to activate your trigger.', + 'Alternative: You can also use Setup → Outbound Messages with a Workflow Rule, but this sends SOAP/XML instead of JSON.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .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 { + 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 { + 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 { + 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 { + 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' }, + } +} diff --git a/apps/sim/triggers/salesforce/webhook.ts b/apps/sim/triggers/salesforce/webhook.ts new file mode 100644 index 00000000000..32d0165db24 --- /dev/null +++ b/apps/sim/triggers/salesforce/webhook.ts @@ -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', + }, + }, +}