Skip to content

Commit 0600c90

Browse files
committed
feat(triggers): enrich Salesforce and Linear webhook output schemas
Salesforce: expose simEventType alongside eventType; pass OwnerId and SystemModstamp on record lifecycle inputs; add AccountId/OwnerId for Opportunity and AccountId/ContactId/OwnerId for Case. Align trigger output docs with Flow JSON payloads and formatInput. Linear: document actor email and profile url per official webhook payload; add Comment data.edited from Linear's sample payload. Tests: extend Salesforce formatInput coverage for new fields.
1 parent 5148936 commit 0600c90

File tree

4 files changed

+73
-2
lines changed

4 files changed

+73
-2
lines changed

apps/sim/lib/webhooks/providers/salesforce.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,12 @@ describe('Salesforce webhook provider', () => {
7878
const { input } = await salesforceHandler.formatInput!({
7979
body: {
8080
eventType: 'created',
81+
simEventType: 'after_insert',
8182
objectType: 'Lead',
8283
Id: '00Q1',
8384
Name: 'Test',
85+
OwnerId: '005OWNER',
86+
SystemModstamp: '2024-01-01T00:00:00.000Z',
8487
},
8588
headers: {},
8689
requestId: 't4',
@@ -89,8 +92,12 @@ describe('Salesforce webhook provider', () => {
8992
})
9093
const i = input as Record<string, unknown>
9194
expect(i.eventType).toBe('created')
95+
expect(i.simEventType).toBe('after_insert')
9296
expect(i.objectType).toBe('Lead')
9397
expect(i.recordId).toBe('00Q1')
98+
const rec = i.record as Record<string, unknown>
99+
expect(rec.OwnerId).toBe('005OWNER')
100+
expect(rec.SystemModstamp).toBe('2024-01-01T00:00:00.000Z')
94101
})
95102

96103
it('extractIdempotencyId includes record id', () => {

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ function pickRecordId(body: Record<string, unknown>, record: Record<string, unkn
9696
return id
9797
}
9898

99+
function pickStr(record: Record<string, unknown>, key: string): string {
100+
const v = record[key]
101+
return typeof v === 'string' ? v : ''
102+
}
103+
99104
export const salesforceHandler: WebhookProviderHandler = {
100105
verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null {
101106
const secret = providerConfig.webhookSecret as string | undefined
@@ -157,6 +162,7 @@ export const salesforceHandler: WebhookProviderHandler = {
157162
(typeof body.eventType === 'string' && body.eventType) ||
158163
(typeof body.simEventType === 'string' && body.simEventType) ||
159164
''
165+
const simEventTypeRaw = typeof body.simEventType === 'string' ? body.simEventType : ''
160166

161167
if (id === 'salesforce_webhook') {
162168
return {
@@ -165,6 +171,7 @@ export const salesforceHandler: WebhookProviderHandler = {
165171
objectType: objectType || '',
166172
recordId,
167173
timestamp,
174+
simEventType: simEventTypeRaw,
168175
record: Object.keys(record).length > 0 ? record : body,
169176
payload: ctx.body,
170177
},
@@ -183,12 +190,15 @@ export const salesforceHandler: WebhookProviderHandler = {
183190
objectType: objectType || '',
184191
recordId,
185192
timestamp,
193+
simEventType: simEventTypeRaw,
186194
record: {
187195
Id: typeof record.Id === 'string' ? record.Id : recordId,
188196
Name: typeof record.Name === 'string' ? record.Name : '',
189197
CreatedDate: typeof record.CreatedDate === 'string' ? record.CreatedDate : '',
190198
LastModifiedDate:
191199
typeof record.LastModifiedDate === 'string' ? record.LastModifiedDate : '',
200+
OwnerId: pickStr(record, 'OwnerId'),
201+
SystemModstamp: pickStr(record, 'SystemModstamp'),
192202
},
193203
changedFields: changedFields !== undefined ? changedFields : null,
194204
payload: ctx.body,
@@ -203,13 +213,16 @@ export const salesforceHandler: WebhookProviderHandler = {
203213
objectType: objectType || 'Opportunity',
204214
recordId,
205215
timestamp,
216+
simEventType: simEventTypeRaw,
206217
record: {
207218
Id: typeof record.Id === 'string' ? record.Id : recordId,
208219
Name: typeof record.Name === 'string' ? record.Name : '',
209220
StageName: typeof record.StageName === 'string' ? record.StageName : '',
210221
Amount: record.Amount !== undefined ? String(record.Amount) : '',
211222
CloseDate: typeof record.CloseDate === 'string' ? record.CloseDate : '',
212223
Probability: record.Probability !== undefined ? String(record.Probability) : '',
224+
AccountId: pickStr(record, 'AccountId'),
225+
OwnerId: pickStr(record, 'OwnerId'),
213226
},
214227
previousStage:
215228
typeof body.previousStage === 'string'
@@ -235,12 +248,16 @@ export const salesforceHandler: WebhookProviderHandler = {
235248
objectType: objectType || 'Case',
236249
recordId,
237250
timestamp,
251+
simEventType: simEventTypeRaw,
238252
record: {
239253
Id: typeof record.Id === 'string' ? record.Id : recordId,
240254
Subject: typeof record.Subject === 'string' ? record.Subject : '',
241255
Status: typeof record.Status === 'string' ? record.Status : '',
242256
Priority: typeof record.Priority === 'string' ? record.Priority : '',
243257
CaseNumber: typeof record.CaseNumber === 'string' ? record.CaseNumber : '',
258+
AccountId: pickStr(record, 'AccountId'),
259+
ContactId: pickStr(record, 'ContactId'),
260+
OwnerId: pickStr(record, 'OwnerId'),
244261
},
245262
previousStatus:
246263
typeof body.previousStatus === 'string'
@@ -265,6 +282,7 @@ export const salesforceHandler: WebhookProviderHandler = {
265282
objectType: objectType || '',
266283
recordId,
267284
timestamp,
285+
simEventType: simEventTypeRaw,
268286
record: Object.keys(record).length > 0 ? record : body,
269287
payload: ctx.body,
270288
},

apps/sim/triggers/linear/utils.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@ export function buildLinearV2SubBlocks(options: {
194194
}
195195

196196
/**
197-
* Shared user/actor output schema
198-
* Note: Linear webhooks only include id, name, and type in actor objects
197+
* Shared user/actor output schema (Linear data-change webhook `actor` object).
198+
* @see https://linear.app/developers/webhooks — actor may be a User, OauthClient, or Integration; `type` is mapped to `actorType` (TriggerOutput reserves nested `type` for field kinds).
199199
*/
200200
export const userOutputs = {
201201
id: {
@@ -211,6 +211,14 @@ export const userOutputs = {
211211
type: 'string',
212212
description: 'Actor type from Linear (e.g. user, OauthClient, Integration)',
213213
},
214+
email: {
215+
type: 'string',
216+
description: 'Actor email (present for user actors in Linear webhook payloads)',
217+
},
218+
url: {
219+
type: 'string',
220+
description: 'Actor profile URL in Linear (distinct from the top-level subject entity `url`)',
221+
},
214222
} as const
215223

216224
/**
@@ -495,6 +503,10 @@ export function buildCommentOutputs(): Record<string, TriggerOutput> {
495503
type: 'string',
496504
description: 'Comment body text',
497505
},
506+
edited: {
507+
type: 'boolean',
508+
description: 'Whether the comment body has been edited (Linear webhook payload field)',
509+
},
498510
url: {
499511
type: 'string',
500512
description: 'Comment URL',

apps/sim/triggers/salesforce/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ export function buildSalesforceRecordOutputs(): Record<string, TriggerOutput> {
234234
type: 'string',
235235
description: 'The type of event (e.g., created, updated, deleted)',
236236
},
237+
/** Present when the Flow JSON body uses `simEventType` instead of or in addition to `eventType`. */
238+
simEventType: {
239+
type: 'string',
240+
description:
241+
'Optional alias from the payload (`simEventType`). Empty when only `eventType` is sent.',
242+
},
237243
objectType: {
238244
type: 'string',
239245
description: 'Salesforce object type (e.g., Account, Contact, Lead)',
@@ -245,6 +251,14 @@ export function buildSalesforceRecordOutputs(): Record<string, TriggerOutput> {
245251
Name: { type: 'string', description: 'Record name' },
246252
CreatedDate: { type: 'string', description: 'Record creation date' },
247253
LastModifiedDate: { type: 'string', description: 'Last modification date' },
254+
OwnerId: {
255+
type: 'string',
256+
description: 'Record owner ID (standard field when sent in the Flow body)',
257+
},
258+
SystemModstamp: {
259+
type: 'string',
260+
description: 'System modstamp from the record (ISO 8601) when included in the payload',
261+
},
248262
},
249263
changedFields: { type: 'json', description: 'Fields that were changed (for update events)' },
250264
payload: { type: 'json', description: 'Full webhook payload' },
@@ -257,6 +271,11 @@ export function buildSalesforceRecordOutputs(): Record<string, TriggerOutput> {
257271
export function buildSalesforceOpportunityStageOutputs(): Record<string, TriggerOutput> {
258272
return {
259273
eventType: { type: 'string', description: 'The type of event' },
274+
simEventType: {
275+
type: 'string',
276+
description:
277+
'Optional alias from the payload (`simEventType`). Empty when only `eventType` is sent.',
278+
},
260279
objectType: { type: 'string', description: 'Salesforce object type (Opportunity)' },
261280
recordId: { type: 'string', description: 'Opportunity ID' },
262281
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
@@ -267,6 +286,8 @@ export function buildSalesforceOpportunityStageOutputs(): Record<string, Trigger
267286
Amount: { type: 'string', description: 'Deal amount' },
268287
CloseDate: { type: 'string', description: 'Expected close date' },
269288
Probability: { type: 'string', description: 'Win probability' },
289+
AccountId: { type: 'string', description: 'Related Account ID (standard Opportunity field)' },
290+
OwnerId: { type: 'string', description: 'Opportunity owner ID' },
270291
},
271292
previousStage: { type: 'string', description: 'Previous stage name' },
272293
newStage: { type: 'string', description: 'New stage name' },
@@ -280,6 +301,11 @@ export function buildSalesforceOpportunityStageOutputs(): Record<string, Trigger
280301
export function buildSalesforceCaseStatusOutputs(): Record<string, TriggerOutput> {
281302
return {
282303
eventType: { type: 'string', description: 'The type of event' },
304+
simEventType: {
305+
type: 'string',
306+
description:
307+
'Optional alias from the payload (`simEventType`). Empty when only `eventType` is sent.',
308+
},
283309
objectType: { type: 'string', description: 'Salesforce object type (Case)' },
284310
recordId: { type: 'string', description: 'Case ID' },
285311
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
@@ -289,6 +315,9 @@ export function buildSalesforceCaseStatusOutputs(): Record<string, TriggerOutput
289315
Status: { type: 'string', description: 'Current case status' },
290316
Priority: { type: 'string', description: 'Case priority' },
291317
CaseNumber: { type: 'string', description: 'Case number' },
318+
AccountId: { type: 'string', description: 'Related Account ID' },
319+
ContactId: { type: 'string', description: 'Related Contact ID' },
320+
OwnerId: { type: 'string', description: 'Case owner ID' },
292321
},
293322
previousStatus: { type: 'string', description: 'Previous case status' },
294323
newStatus: { type: 'string', description: 'New case status' },
@@ -302,6 +331,11 @@ export function buildSalesforceCaseStatusOutputs(): Record<string, TriggerOutput
302331
export function buildSalesforceWebhookOutputs(): Record<string, TriggerOutput> {
303332
return {
304333
eventType: { type: 'string', description: 'The type of event' },
334+
simEventType: {
335+
type: 'string',
336+
description:
337+
'Optional alias from the payload (`simEventType`). Empty when only `eventType` is sent.',
338+
},
305339
objectType: { type: 'string', description: 'Salesforce object type' },
306340
recordId: { type: 'string', description: 'ID of the affected record' },
307341
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },

0 commit comments

Comments
 (0)