diff --git a/.claude/commands/add-trigger.md b/.claude/commands/add-trigger.md index b65edf42a..d252bf616 100644 --- a/.claude/commands/add-trigger.md +++ b/.claude/commands/add-trigger.md @@ -552,6 +552,53 @@ All fields automatically have: - `mode: 'trigger'` - Only shown in trigger mode - `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected +## Trigger Outputs & Webhook Input Formatting + +### Important: Two Sources of Truth + +There are two related but separate concerns: + +1. **Trigger `outputs`** - Schema/contract defining what fields SHOULD be available. Used by UI for tag dropdown. +2. **`formatWebhookInput`** - Implementation that transforms raw webhook payload into actual data. Located in `apps/sim/lib/webhooks/utils.server.ts`. + +**These MUST be aligned.** The fields returned by `formatWebhookInput` should match what's defined in trigger `outputs`. If they differ: +- Tag dropdown shows fields that don't exist (broken variable resolution) +- Or actual data has fields not shown in dropdown (users can't discover them) + +### When to Add a formatWebhookInput Handler + +- **Simple providers**: If the raw webhook payload structure already matches your outputs, you don't need a handler. The generic fallback returns `body` directly. +- **Complex providers**: If you need to transform, flatten, extract nested data, compute fields, or handle conditional logic, add a handler. + +### Adding a Handler + +In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block: + +```typescript +if (foundWebhook.provider === '{service}') { + // Transform raw webhook body to match trigger outputs + return { + eventType: body.type, + resourceId: body.data?.id || '', + timestamp: body.created_at, + resource: body.data, + } +} +``` + +**Key rules:** +- Return fields that match your trigger `outputs` definition exactly +- No wrapper objects like `webhook: { data: ... }` or `{service}: { ... }` +- No duplication (don't spread body AND add individual fields) +- Use `null` for missing optional data, not empty objects with empty strings + +### Verify Alignment + +Run the alignment checker: +```bash +bunx scripts/check-trigger-alignment.ts {service} +``` + ## Trigger Outputs Trigger outputs use the same schema as block outputs (NOT tool outputs). @@ -649,6 +696,11 @@ export const {service}WebhookTrigger: TriggerConfig = { - [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts` - [ ] Added provider to `cleanupExternalWebhook` function +### Webhook Input Formatting +- [ ] Added handler in `apps/sim/lib/webhooks/utils.server.ts` (if custom formatting needed) +- [ ] Handler returns fields matching trigger `outputs` exactly +- [ ] Run `bunx scripts/check-trigger-alignment.ts {service}` to verify alignment + ### Testing - [ ] Run `bun run type-check` to verify no TypeScript errors - [ ] Restart dev server to pick up new triggers diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 06752d5d6..865294d20 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -313,6 +313,26 @@ export const getBlock = (type: string): BlockConfig | undefined => { return registry[normalized] } +export const getLatestBlock = (baseType: string): BlockConfig | undefined => { + const normalized = baseType.replace(/-/g, '_') + + const versionedKeys = Object.keys(registry).filter((key) => { + const match = key.match(new RegExp(`^${normalized}_v(\\d+)$`)) + return match !== null + }) + + if (versionedKeys.length > 0) { + const sorted = versionedKeys.sort((a, b) => { + const versionA = Number.parseInt(a.match(/_v(\d+)$/)?.[1] || '0', 10) + const versionB = Number.parseInt(b.match(/_v(\d+)$/)?.[1] || '0', 10) + return versionB - versionA + }) + return registry[sorted[0]] + } + + return registry[normalized] +} + export const getBlockByToolName = (toolName: string): BlockConfig | undefined => { return Object.values(registry).find((block) => block.tools?.access?.includes(toolName)) } diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts index f6c9753bb..1ed90c371 100644 --- a/apps/sim/executor/utils/start-block.ts +++ b/apps/sim/executor/utils/start-block.ts @@ -378,21 +378,10 @@ function buildManualTriggerOutput( } function buildIntegrationTriggerOutput( - finalInput: unknown, + _finalInput: unknown, workflowInput: unknown ): NormalizedBlockOutput { - const base: NormalizedBlockOutput = isPlainObject(workflowInput) - ? ({ ...(workflowInput as Record) } as NormalizedBlockOutput) - : {} - - if (isPlainObject(finalInput)) { - Object.assign(base, finalInput as Record) - base.input = { ...(finalInput as Record) } - } else { - base.input = finalInput - } - - return mergeFilesIntoOutput(base, workflowInput) + return isPlainObject(workflowInput) ? (workflowInput as NormalizedBlockOutput) : {} } function extractSubBlocks(block: SerializedBlock): Record | undefined { diff --git a/apps/sim/lib/logs/get-trigger-options.ts b/apps/sim/lib/logs/get-trigger-options.ts index fd704c775..6aad83ae0 100644 --- a/apps/sim/lib/logs/get-trigger-options.ts +++ b/apps/sim/lib/logs/get-trigger-options.ts @@ -1,4 +1,4 @@ -import { getBlock } from '@/blocks/registry' +import { getLatestBlock } from '@/blocks/registry' import { getAllTriggers } from '@/triggers' export interface TriggerOption { @@ -49,22 +49,13 @@ export function getTriggerOptions(): TriggerOption[] { continue } - const block = getBlock(provider) + const block = getLatestBlock(provider) - if (block) { - providerMap.set(provider, { - value: provider, - label: block.name, // Use block's display name (e.g., "Slack", "GitHub") - color: block.bgColor || '#6b7280', // Use block's hex color, fallback to gray - }) - } else { - const label = formatProviderName(provider) - providerMap.set(provider, { - value: provider, - label, - color: '#6b7280', // gray fallback - }) - } + providerMap.set(provider, { + value: provider, + label: block?.name || formatProviderName(provider), + color: block?.bgColor || '#6b7280', + }) } const integrationOptions = Array.from(providerMap.values()).sort((a, b) => diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index 02fbac769..923cfc650 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -177,18 +177,10 @@ async function formatTeamsGraphNotification( keys: Object.keys(body || {}), }) return { - input: 'Teams notification received', - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook?.path || '', - providerConfig: foundWebhook?.providerConfig || {}, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + from: null, + message: { raw: body }, + activity: body, + conversation: null, } } const resolvedChatId = chatId as string @@ -431,31 +423,12 @@ async function formatTeamsGraphNotification( hasCredential: !!credentialId, }) return { - input: '', - message_id: messageId, - chat_id: chatId, - from_name: 'Unknown', + message_id: resolvedMessageId, + chat_id: resolvedChatId, + from_name: '', text: '', - created_at: notification.resourceData?.createdDateTime || '', - change_type: changeType, - subscription_id: subscriptionId, + created_at: '', attachments: [], - microsoftteams: { - message: { id: messageId, text: '', timestamp: '', chatId, raw: null }, - from: { id: '', name: 'Unknown', aadObjectId: '' }, - notification: { changeType, subscriptionId, resource }, - }, - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook?.path || '', - providerConfig: foundWebhook?.providerConfig || {}, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -464,45 +437,12 @@ async function formatTeamsGraphNotification( const createdAt = message.createdDateTime || '' return { - input: messageText, - message_id: messageId, - chat_id: chatId, - from_name: from.displayName || 'Unknown', + message_id: resolvedMessageId, + chat_id: resolvedChatId, + from_name: from.displayName || '', text: messageText, created_at: createdAt, - change_type: changeType, - subscription_id: subscriptionId, attachments: rawAttachments, - microsoftteams: { - message: { - id: messageId, - text: messageText, - timestamp: createdAt, - chatId, - raw: message, - }, - from: { - id: from.id, - name: from.displayName, - aadObjectId: from.aadObjectId, - }, - notification: { - changeType, - subscriptionId, - resource, - }, - }, - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook?.path || '', - providerConfig: foundWebhook?.providerConfig || {}, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -587,166 +527,73 @@ export async function formatWebhookInput( if (messages.length > 0) { const message = messages[0] - const phoneNumberId = data.metadata?.phone_number_id - const from = message.from - const messageId = message.id - const timestamp = message.timestamp - const text = message.text?.body - return { - whatsapp: { - data: { - messageId, - from, - phoneNumberId, - text, - timestamp, - raw: message, - }, - }, - webhook: { - data: { - provider: 'whatsapp', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + messageId: message.id, + from: message.from, + phoneNumberId: data.metadata?.phone_number_id, + text: message.text?.body, + timestamp: message.timestamp, + raw: JSON.stringify(message), } } return null } if (foundWebhook.provider === 'telegram') { - const message = + const rawMessage = body?.message || body?.edited_message || body?.channel_post || body?.edited_channel_post - if (message) { - let input = '' + const updateType = body.message + ? 'message' + : body.edited_message + ? 'edited_message' + : body.channel_post + ? 'channel_post' + : body.edited_channel_post + ? 'edited_channel_post' + : 'unknown' - if (message.text) { - input = message.text - } else if (message.caption) { - input = message.caption - } else if (message.photo) { - input = 'Photo message' - } else if (message.document) { - input = `Document: ${message.document.file_name || 'file'}` - } else if (message.audio) { - input = `Audio: ${message.audio.title || 'audio file'}` - } else if (message.video) { - input = 'Video message' - } else if (message.voice) { - input = 'Voice message' - } else if (message.sticker) { - input = `Sticker: ${message.sticker.emoji || '🎭'}` - } else if (message.location) { - input = 'Location shared' - } else if (message.contact) { - input = `Contact: ${message.contact.first_name || 'contact'}` - } else if (message.poll) { - input = `Poll: ${message.poll.question}` - } else { - input = 'Message received' - } - - const messageObj = { - id: message.message_id, - text: message.text, - caption: message.caption, - date: message.date, - messageType: message.photo - ? 'photo' - : message.document - ? 'document' - : message.audio - ? 'audio' - : message.video - ? 'video' - : message.voice - ? 'voice' - : message.sticker - ? 'sticker' - : message.location - ? 'location' - : message.contact - ? 'contact' - : message.poll - ? 'poll' - : 'text', - raw: message, - } - - const senderObj = message.from - ? { - id: message.from.id, - firstName: message.from.first_name, - lastName: message.from.last_name, - username: message.from.username, - languageCode: message.from.language_code, - isBot: message.from.is_bot, - } - : null - - const chatObj = message.chat - ? { - id: message.chat.id, - type: message.chat.type, - title: message.chat.title, - username: message.chat.username, - firstName: message.chat.first_name, - lastName: message.chat.last_name, - } - : null + if (rawMessage) { + const messageType = rawMessage.photo + ? 'photo' + : rawMessage.document + ? 'document' + : rawMessage.audio + ? 'audio' + : rawMessage.video + ? 'video' + : rawMessage.voice + ? 'voice' + : rawMessage.sticker + ? 'sticker' + : rawMessage.location + ? 'location' + : rawMessage.contact + ? 'contact' + : rawMessage.poll + ? 'poll' + : 'text' return { - input, - - // Top-level properties for backward compatibility with syntax - message: messageObj, - sender: senderObj, - chat: chatObj, + message: { + id: rawMessage.message_id, + text: rawMessage.text, + date: rawMessage.date, + messageType, + raw: rawMessage, + }, + sender: rawMessage.from + ? { + id: rawMessage.from.id, + username: rawMessage.from.username, + firstName: rawMessage.from.first_name, + lastName: rawMessage.from.last_name, + languageCode: rawMessage.from.language_code, + isBot: rawMessage.from.is_bot, + } + : null, updateId: body.update_id, - updateType: body.message - ? 'message' - : body.edited_message - ? 'edited_message' - : body.channel_post - ? 'channel_post' - : body.edited_channel_post - ? 'edited_channel_post' - : 'unknown', - - // Keep the nested structure for the new telegram.message.text syntax - telegram: { - message: messageObj, - sender: senderObj, - chat: chatObj, - updateId: body.update_id, - updateType: body.message - ? 'message' - : body.edited_message - ? 'edited_message' - : body.channel_post - ? 'channel_post' - : body.edited_channel_post - ? 'edited_channel_post' - : 'unknown', - }, - webhook: { - data: { - provider: 'telegram', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + updateType, } } @@ -756,23 +603,8 @@ export async function formatWebhookInput( }) return { - input: 'Telegram update received', - telegram: { - updateId: body.update_id, - updateType: 'unknown', - raw: body, - }, - webhook: { - data: { - provider: 'telegram', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + updateId: body.update_id, + updateType, } } @@ -810,40 +642,15 @@ export async function formatWebhookInput( callerZip: body.CallerZip, callerCountry: body.CallerCountry, callToken: body.CallToken, - - webhook: { - data: { - provider: 'twilio_voice', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + raw: JSON.stringify(body), } } if (foundWebhook.provider === 'gmail') { if (body && typeof body === 'object' && 'email' in body) { - const email = body.email as Record - const timestamp = body.timestamp return { - ...email, - email, - ...(timestamp !== undefined && { timestamp }), - webhook: { - data: { - provider: 'gmail', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + email: body.email, + timestamp: body.timestamp, } } return body @@ -851,23 +658,9 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'outlook') { if (body && typeof body === 'object' && 'email' in body) { - const email = body.email as Record - const timestamp = body.timestamp return { - ...email, - email, - ...(timestamp !== undefined && { timestamp }), - webhook: { - data: { - provider: 'outlook', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + email: body.email, + timestamp: body.timestamp, } } return body @@ -875,26 +668,10 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'rss') { if (body && typeof body === 'object' && 'item' in body) { - const item = body.item as Record - const feed = body.feed as Record - return { - title: item?.title, - link: item?.link, - pubDate: item?.pubDate, - item, - feed, - webhook: { - data: { - provider: 'rss', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + item: body.item, + feed: body.feed, + timestamp: body.timestamp, } } return body @@ -902,32 +679,9 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'imap') { if (body && typeof body === 'object' && 'email' in body) { - const email = body.email as Record return { - messageId: email?.messageId, - subject: email?.subject, - from: email?.from, - to: email?.to, - cc: email?.cc, - date: email?.date, - bodyText: email?.bodyText, - bodyHtml: email?.bodyHtml, - mailbox: email?.mailbox, - hasAttachments: email?.hasAttachments, - attachments: email?.attachments, - email, + email: body.email, timestamp: body.timestamp, - webhook: { - data: { - provider: 'imap', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } return body @@ -952,7 +706,6 @@ export async function formatWebhookInput( payload: body, provider: 'hubspot', providerConfig: foundWebhook.providerConfig, - workflowId: foundWorkflow.id, } } @@ -997,24 +750,10 @@ export async function formatWebhookInput( const activityObj = body || {} return { - input: messageText, - from: fromObj, message: messageObj, activity: activityObj, conversation: conversationObj, - - webhook: { - data: { - provider: 'microsoft-teams', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1022,46 +761,19 @@ export async function formatWebhookInput( const event = body?.event if (event && body?.type === 'event_callback') { - let input = '' - - if (event.text) { - input = event.text - } else if (event.type === 'app_mention') { - input = 'App mention received' - } else { - input = 'Slack event received' - } - - const eventObj = { - event_type: event.type || '', - channel: event.channel || '', - channel_name: '', - user: event.user || '', - user_name: '', - text: event.text || '', - timestamp: event.ts || event.event_ts || '', - team_id: body.team_id || event.team || '', - event_id: body.event_id || '', - } - return { - input, - - event: eventObj, - slack: { - event: eventObj, + event: { + event_type: event.type || '', + channel: event.channel || '', + channel_name: '', + user: event.user || '', + user_name: '', + text: event.text || '', + timestamp: event.ts || event.event_ts || '', + thread_ts: event.thread_ts || '', + team_id: body.team_id || event.team || '', + event_id: body.event_id || '', }, - webhook: { - data: { - provider: 'slack', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1072,80 +784,31 @@ export async function formatWebhookInput( }) return { - input: 'Slack webhook received', - slack: { - event: { - event_type: body?.event?.type || body?.type || 'unknown', - channel: body?.event?.channel || '', - user: body?.event?.user || '', - text: body?.event?.text || '', - timestamp: body?.event?.ts || '', - team_id: body?.team_id || '', - event_id: body?.event_id || '', - }, + event: { + event_type: body?.event?.type || body?.type || 'unknown', + channel: body?.event?.channel || '', + channel_name: '', + user: body?.event?.user || '', + user_name: '', + text: body?.event?.text || '', + timestamp: body?.event?.ts || '', + thread_ts: body?.event?.thread_ts || '', + team_id: body?.team_id || '', + event_id: body?.event_id || '', }, - webhook: { - data: { - provider: 'slack', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } if (foundWebhook.provider === 'webflow') { - const triggerType = body?.triggerType || 'unknown' - const siteId = body?.siteId || '' - const workspaceId = body?.workspaceId || '' - const collectionId = body?.collectionId || '' - const payload = body?.payload || {} - const formId = body?.formId || '' - const formName = body?.name || '' - const formSubmissionId = body?.id || '' - const submittedAt = body?.submittedAt || '' - const formData = body?.data || {} - const schema = body?.schema || {} - return { - siteId, - workspaceId, - collectionId, - payload, - triggerType, - - formId, - name: formName, - id: formSubmissionId, - submittedAt, - data: formData, - schema, + siteId: body?.siteId || '', + formId: body?.formId || '', + name: body?.name || '', + id: body?.id || '', + submittedAt: body?.submittedAt || '', + data: body?.data || {}, + schema: body?.schema || {}, formElementId: body?.formElementId || '', - - webflow: { - siteId, - workspaceId, - collectionId, - payload, - triggerType, - raw: body, - }, - - webhook: { - data: { - provider: 'webflow', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1156,7 +819,6 @@ export async function formatWebhookInput( if (foundWebhook.provider === 'google_forms') { const providerConfig = (foundWebhook.providerConfig as Record) || {} - // Normalize answers: if value is an array with single element, collapse to scalar; keep multi-select arrays const normalizeAnswers = (src: unknown): Record => { if (!src || typeof src !== 'object') return {} const out: Record = {} @@ -1176,205 +838,47 @@ export async function formatWebhookInput( const formId = body?.formId || providerConfig.formId || '' const includeRaw = providerConfig.includeRawPayload !== false - const normalizedAnswers = normalizeAnswers(body?.answers) - - const summaryCount = Object.keys(normalizedAnswers).length - const input = `Google Form response${responseId ? ` ${responseId}` : ''} (${summaryCount} answers)` - return { - input, responseId, createTime, lastSubmittedTime, formId, - answers: normalizedAnswers, + answers: normalizeAnswers(body?.answers), ...(includeRaw ? { raw: body?.raw ?? body } : {}), - google_forms: { - responseId, - createTime, - lastSubmittedTime, - formId, - answers: normalizedAnswers, - ...(includeRaw ? { raw: body?.raw ?? body } : {}), - }, - webhook: { - data: { - provider: 'google_forms', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: includeRaw ? body : undefined, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } if (foundWebhook.provider === 'github') { - // GitHub webhook input formatting logic const eventType = request.headers.get('x-github-event') || 'unknown' - const delivery = request.headers.get('x-github-delivery') || '' - - // Extract common GitHub properties - const repository = body?.repository || {} - const sender = body?.sender || {} - const action = body?.action || '' - - // Build GitHub-specific variables based on the trigger config outputs - const githubData = { - // Event metadata - event_type: eventType, - action: action, - delivery_id: delivery, - - // Repository information (avoid 'repository' to prevent conflict with the object) - repository_full_name: repository.full_name || '', - repository_name: repository.name || '', - repository_owner: repository.owner?.login || '', - repository_id: repository.id || '', - repository_url: repository.html_url || '', - - // Sender information (avoid 'sender' to prevent conflict with the object) - sender_login: sender.login || '', - sender_id: sender.id || '', - sender_type: sender.type || '', - sender_url: sender.html_url || '', - - // Event-specific data - ...(body?.ref && { - ref: body.ref, - branch: body.ref?.replace('refs/heads/', '') || '', - }), - ...(body?.before && { before: body.before }), - ...(body?.after && { after: body.after }), - ...(body?.commits && { - commits: JSON.stringify(body.commits), - commit_count: body.commits.length || 0, - }), - ...(body?.head_commit && { - commit_message: body.head_commit.message || '', - commit_author: body.head_commit.author?.name || '', - commit_sha: body.head_commit.id || '', - commit_url: body.head_commit.url || '', - }), - ...(body?.pull_request && { - pull_request: JSON.stringify(body.pull_request), - pr_number: body.pull_request.number || '', - pr_title: body.pull_request.title || '', - pr_state: body.pull_request.state || '', - pr_url: body.pull_request.html_url || '', - }), - ...(body?.issue && { - issue: JSON.stringify(body.issue), - issue_number: body.issue.number || '', - issue_title: body.issue.title || '', - issue_state: body.issue.state || '', - issue_url: body.issue.html_url || '', - }), - ...(body?.comment && { - comment: JSON.stringify(body.comment), - comment_body: body.comment.body || '', - comment_url: body.comment.html_url || '', - }), - } - - // Set input based on event type for workflow processing - let input = '' - switch (eventType) { - case 'push': - input = `Push to ${githubData.branch || githubData.ref}: ${githubData.commit_message || 'No commit message'}` - break - case 'pull_request': - input = `${action} pull request: ${githubData.pr_title || 'No title'}` - break - case 'issues': - input = `${action} issue: ${githubData.issue_title || 'No title'}` - break - case 'issue_comment': - case 'pull_request_review_comment': - input = `Comment ${action}: ${githubData.comment_body?.slice(0, 100) || 'No comment body'}${(githubData.comment_body?.length || 0) > 100 ? '...' : ''}` - break - default: - input = `GitHub ${eventType} event${action ? ` (${action})` : ''}` - } + const branch = body?.ref?.replace('refs/heads/', '') || '' return { ...body, - webhook: { - data: { - provider: 'github', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + event_type: eventType, + action: body?.action || '', + branch, } } if (foundWebhook.provider === 'typeform') { - const eventId = body?.event_id || '' - const eventType = body?.event_type || 'form_response' const formResponse = body?.form_response || {} - const formId = formResponse.form_id || '' - const token = formResponse.token || '' - const submittedAt = formResponse.submitted_at || '' - const landedAt = formResponse.landed_at || '' - const calculated = formResponse.calculated || {} - const variables = formResponse.variables || [] - const hidden = formResponse.hidden || {} - const answers = formResponse.answers || [] - const definition = formResponse.definition || {} - const ending = formResponse.ending || {} - const providerConfig = (foundWebhook.providerConfig as Record) || {} const includeDefinition = providerConfig.includeDefinition === true return { - event_id: eventId, - event_type: eventType, - form_id: formId, - token, - submitted_at: submittedAt, - landed_at: landedAt, - calculated, - variables, - hidden, - answers, - ...(includeDefinition ? { definition } : {}), - ending, - - typeform: { - event_id: eventId, - event_type: eventType, - form_id: formId, - token, - submitted_at: submittedAt, - landed_at: landedAt, - calculated, - variables, - hidden, - answers, - ...(includeDefinition ? { definition } : {}), - ending, - }, - + event_id: body?.event_id || '', + event_type: body?.event_type || 'form_response', + form_id: formResponse.form_id || '', + token: formResponse.token || '', + submitted_at: formResponse.submitted_at || '', + landed_at: formResponse.landed_at || '', + calculated: formResponse.calculated || {}, + variables: formResponse.variables || [], + hidden: formResponse.hidden || {}, + answers: formResponse.answers || [], + ...(includeDefinition ? { definition: formResponse.definition || {} } : {}), + ending: formResponse.ending || {}, raw: body, - - webhook: { - data: { - provider: 'typeform', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1389,17 +893,6 @@ export async function formatWebhookInput( actor: body.actor || null, data: body.data || null, updatedFrom: body.updatedFrom || null, - webhook: { - data: { - provider: 'linear', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1411,46 +904,17 @@ export async function formatWebhookInput( const providerConfig = (foundWebhook.providerConfig as Record) || {} const triggerId = providerConfig.triggerId as string | undefined - let extractedData if (triggerId === 'jira_issue_commented') { - extractedData = extractCommentData(body) - } else if (triggerId === 'jira_worklog_created') { - extractedData = extractWorklogData(body) - } else { - extractedData = extractIssueData(body) + return extractCommentData(body) } - - return { - ...extractedData, - webhook: { - data: { - provider: 'jira', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, + if (triggerId === 'jira_worklog_created') { + return extractWorklogData(body) } + return extractIssueData(body) } if (foundWebhook.provider === 'stripe') { - return { - ...body, - webhook: { - data: { - provider: 'stripe', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, - } + return body } if (foundWebhook.provider === 'calendly') { @@ -1459,17 +923,6 @@ export async function formatWebhookInput( created_at: body.created_at, created_by: body.created_by, payload: body.payload, - webhook: { - data: { - provider: 'calendly', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1489,17 +942,6 @@ export async function formatWebhookInput( transcript: body.transcript || [], insights: body.insights || {}, meeting: body, - webhook: { - data: { - provider: 'circleback', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } @@ -1508,57 +950,18 @@ export async function formatWebhookInput( type: body.type, user_id: body.user_id, data: body.data || {}, - - webhook: { - data: { - provider: 'grain', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } if (foundWebhook.provider === 'fireflies') { - // Fireflies webhook payload uses camelCase: - // { meetingId, eventType, clientReferenceId } return { meetingId: body.meetingId || '', eventType: body.eventType || 'Transcription completed', clientReferenceId: body.clientReferenceId || '', - - webhook: { - data: { - provider: 'fireflies', - path: foundWebhook.path, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, } } - // Generic format for other providers - return { - webhook: { - data: { - path: foundWebhook.path, - provider: foundWebhook.provider, - providerConfig: foundWebhook.providerConfig, - payload: body, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - }, - }, - workflowId: foundWorkflow.id, - } + return body } /** diff --git a/apps/sim/lib/workflows/comparison/compare.test.ts b/apps/sim/lib/workflows/comparison/compare.test.ts index d0abb0040..31af020e3 100644 --- a/apps/sim/lib/workflows/comparison/compare.test.ts +++ b/apps/sim/lib/workflows/comparison/compare.test.ts @@ -2290,7 +2290,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: null }, }, }), @@ -2302,7 +2302,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, }, }), @@ -2318,7 +2318,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, triggerPath: { value: '' }, }, }), @@ -2330,7 +2330,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, triggerPath: { value: '/api/webhooks/abc123' }, }, }), @@ -2346,7 +2346,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: null }, triggerPath: { value: '' }, }, @@ -2359,7 +2359,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, triggerPath: { value: '/api/webhooks/abc123' }, }, @@ -2371,14 +2371,18 @@ describe('hasWorkflowChanged', () => { }) it.concurrent( - 'should detect change when triggerConfig differs but runtime metadata also differs', + 'should detect change when actual config differs but runtime metadata also differs', () => { + // Test that when a real config field changes along with runtime metadata, + // the change is still detected. Using 'model' as the config field since + // triggerConfig is now excluded from comparison (individual trigger fields + // are compared separately). const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: null }, }, }), @@ -2390,7 +2394,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'pull_request' } }, + model: { value: 'gpt-4o' }, webhookId: { value: 'wh_123456' }, }, }), @@ -2402,8 +2406,12 @@ describe('hasWorkflowChanged', () => { ) it.concurrent( - 'should not detect change when runtime metadata is added to current state', + 'should not detect change when triggerConfig differs (individual fields compared separately)', () => { + // triggerConfig is excluded from comparison because: + // 1. Individual trigger fields are stored as separate subblocks and compared individually + // 2. The client populates triggerConfig with default values from trigger definitions, + // which aren't present in the deployed state, causing false positive change detection const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { @@ -2420,7 +2428,36 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + triggerConfig: { value: { event: 'pull_request', extraField: true } }, + }, + }), + }, + }) + + expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) + } + ) + + it.concurrent( + 'should not detect change when runtime metadata is added to current state', + () => { + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + type: 'starter', + subBlocks: { + model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, triggerPath: { value: '/api/webhooks/abc123' }, }, @@ -2440,7 +2477,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, webhookId: { value: 'wh_old123' }, triggerPath: { value: '/api/webhooks/old' }, }, @@ -2453,7 +2490,7 @@ describe('hasWorkflowChanged', () => { block1: createBlock('block1', { type: 'starter', subBlocks: { - triggerConfig: { value: { event: 'push' } }, + model: { value: 'gpt-4' }, }, }), }, diff --git a/apps/sim/triggers/circleback/utils.ts b/apps/sim/triggers/circleback/utils.ts index a9480cc0a..bee5bd392 100644 --- a/apps/sim/triggers/circleback/utils.ts +++ b/apps/sim/triggers/circleback/utils.ts @@ -96,23 +96,3 @@ export function buildMeetingOutputs(): Record { }, } as Record } - -/** - * Build output schema for generic webhook events - */ -export function buildGenericOutputs(): Record { - return { - payload: { - type: 'object', - description: 'Raw webhook payload', - }, - headers: { - type: 'object', - description: 'Request headers', - }, - timestamp: { - type: 'string', - description: 'ISO8601 received timestamp', - }, - } as Record -} diff --git a/apps/sim/triggers/circleback/webhook.ts b/apps/sim/triggers/circleback/webhook.ts index 017a066b9..f618deaf8 100644 --- a/apps/sim/triggers/circleback/webhook.ts +++ b/apps/sim/triggers/circleback/webhook.ts @@ -1,6 +1,6 @@ import { CirclebackIcon } from '@/components/icons' import type { TriggerConfig } from '@/triggers/types' -import { buildGenericOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils' +import { buildMeetingOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils' export const circlebackWebhookTrigger: TriggerConfig = { id: 'circleback_webhook', @@ -74,7 +74,7 @@ export const circlebackWebhookTrigger: TriggerConfig = { }, ], - outputs: buildGenericOutputs(), + outputs: buildMeetingOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/constants.ts b/apps/sim/triggers/constants.ts index 6c082b76f..354994db0 100644 --- a/apps/sim/triggers/constants.ts +++ b/apps/sim/triggers/constants.ts @@ -31,8 +31,14 @@ export const TRIGGER_PERSISTED_SUBBLOCK_IDS: string[] = [ /** * Trigger-related subblock IDs that represent runtime metadata. They should remain * in the workflow state but must not be modified or cleared by diff operations. + * + * Note: 'triggerConfig' is included because it's an aggregate of individual trigger + * field subblocks. Those individual fields are compared separately, so comparing + * triggerConfig would be redundant. Additionally, the client populates triggerConfig + * with default values from the trigger definition on load, which aren't present in + * the deployed state, causing false positive change detection. */ -export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = ['webhookId', 'triggerPath'] +export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = ['webhookId', 'triggerPath', 'triggerConfig'] /** * Maximum number of consecutive failures before a trigger (schedule/webhook) is auto-disabled. diff --git a/apps/sim/triggers/github/issue_closed.ts b/apps/sim/triggers/github/issue_closed.ts index f2bbe12e3..aa22275a3 100644 --- a/apps/sim/triggers/github/issue_closed.ts +++ b/apps/sim/triggers/github/issue_closed.ts @@ -116,6 +116,11 @@ export const githubIssueClosedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: + 'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)', + }, action: { type: 'string', description: 'Action performed (opened, closed, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/issue_comment.ts b/apps/sim/triggers/github/issue_comment.ts index 972d24412..db40982e9 100644 --- a/apps/sim/triggers/github/issue_comment.ts +++ b/apps/sim/triggers/github/issue_comment.ts @@ -117,6 +117,10 @@ export const githubIssueCommentTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)', + }, action: { type: 'string', description: 'Action performed (created, edited, deleted)', diff --git a/apps/sim/triggers/github/issue_opened.ts b/apps/sim/triggers/github/issue_opened.ts index e05caad03..da4b2e1f2 100644 --- a/apps/sim/triggers/github/issue_opened.ts +++ b/apps/sim/triggers/github/issue_opened.ts @@ -137,6 +137,11 @@ export const githubIssueOpenedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: + 'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)', + }, action: { type: 'string', description: 'Action performed (opened, closed, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_closed.ts b/apps/sim/triggers/github/pr_closed.ts index b60c1043f..a654c0da4 100644 --- a/apps/sim/triggers/github/pr_closed.ts +++ b/apps/sim/triggers/github/pr_closed.ts @@ -117,6 +117,10 @@ export const githubPRClosedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)', + }, action: { type: 'string', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_comment.ts b/apps/sim/triggers/github/pr_comment.ts index 2fab088a7..70b5f9a5c 100644 --- a/apps/sim/triggers/github/pr_comment.ts +++ b/apps/sim/triggers/github/pr_comment.ts @@ -117,6 +117,10 @@ export const githubPRCommentTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)', + }, action: { type: 'string', description: 'Action performed (created, edited, deleted)', diff --git a/apps/sim/triggers/github/pr_merged.ts b/apps/sim/triggers/github/pr_merged.ts index 23ecdad3a..24b2b8205 100644 --- a/apps/sim/triggers/github/pr_merged.ts +++ b/apps/sim/triggers/github/pr_merged.ts @@ -116,6 +116,10 @@ export const githubPRMergedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)', + }, action: { type: 'string', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_opened.ts b/apps/sim/triggers/github/pr_opened.ts index cced084ba..3288cc0c6 100644 --- a/apps/sim/triggers/github/pr_opened.ts +++ b/apps/sim/triggers/github/pr_opened.ts @@ -116,6 +116,10 @@ export const githubPROpenedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)', + }, action: { type: 'string', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', diff --git a/apps/sim/triggers/github/pr_reviewed.ts b/apps/sim/triggers/github/pr_reviewed.ts index a5affcd83..8105f983f 100644 --- a/apps/sim/triggers/github/pr_reviewed.ts +++ b/apps/sim/triggers/github/pr_reviewed.ts @@ -117,6 +117,10 @@ export const githubPRReviewedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request_review)', + }, action: { type: 'string', description: 'Action performed (submitted, edited, dismissed)', diff --git a/apps/sim/triggers/github/push.ts b/apps/sim/triggers/github/push.ts index 789211024..36ce192e5 100644 --- a/apps/sim/triggers/github/push.ts +++ b/apps/sim/triggers/github/push.ts @@ -116,6 +116,14 @@ export const githubPushTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., push)', + }, + branch: { + type: 'string', + description: 'Branch name derived from ref (e.g., main from refs/heads/main)', + }, ref: { type: 'string', description: 'Git reference that was pushed (e.g., refs/heads/main)', diff --git a/apps/sim/triggers/github/release_published.ts b/apps/sim/triggers/github/release_published.ts index 3d10bb742..7e8698d5a 100644 --- a/apps/sim/triggers/github/release_published.ts +++ b/apps/sim/triggers/github/release_published.ts @@ -116,6 +116,10 @@ export const githubReleasePublishedTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., release)', + }, action: { type: 'string', description: diff --git a/apps/sim/triggers/github/workflow_run.ts b/apps/sim/triggers/github/workflow_run.ts index 65e805330..dc30c81b2 100644 --- a/apps/sim/triggers/github/workflow_run.ts +++ b/apps/sim/triggers/github/workflow_run.ts @@ -117,6 +117,10 @@ export const githubWorkflowRunTrigger: TriggerConfig = { ], outputs: { + event_type: { + type: 'string', + description: 'GitHub event type from X-GitHub-Event header (e.g., workflow_run)', + }, action: { type: 'string', description: 'Action performed (requested, in_progress, completed)', diff --git a/apps/sim/triggers/jira/utils.ts b/apps/sim/triggers/jira/utils.ts index 3a3386e9c..15fdab162 100644 --- a/apps/sim/triggers/jira/utils.ts +++ b/apps/sim/triggers/jira/utils.ts @@ -265,11 +265,6 @@ function buildBaseWebhookOutputs(): Record { }, }, }, - - webhook: { - type: 'json', - description: 'Webhook metadata including provider, path, and raw payload', - }, } } diff --git a/apps/sim/triggers/lemlist/email_bounced.ts b/apps/sim/triggers/lemlist/email_bounced.ts index cd7918471..cda91d0b6 100644 --- a/apps/sim/triggers/lemlist/email_bounced.ts +++ b/apps/sim/triggers/lemlist/email_bounced.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailBouncedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailBouncedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_bounced'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailBouncedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_clicked.ts b/apps/sim/triggers/lemlist/email_clicked.ts index a1da3ff6b..e618ef025 100644 --- a/apps/sim/triggers/lemlist/email_clicked.ts +++ b/apps/sim/triggers/lemlist/email_clicked.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailClickedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailClickedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_clicked'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailClickedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_opened.ts b/apps/sim/triggers/lemlist/email_opened.ts index 12c6638e8..e6ac4c574 100644 --- a/apps/sim/triggers/lemlist/email_opened.ts +++ b/apps/sim/triggers/lemlist/email_opened.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailOpenedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailOpenedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_opened'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailOpenedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_replied.ts b/apps/sim/triggers/lemlist/email_replied.ts index bb95476b3..be2dc4152 100644 --- a/apps/sim/triggers/lemlist/email_replied.ts +++ b/apps/sim/triggers/lemlist/email_replied.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildEmailReplyOutputs, + buildEmailRepliedOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -30,7 +30,7 @@ export const lemlistEmailRepliedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_replied'), }), - outputs: buildEmailReplyOutputs(), + outputs: buildEmailRepliedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/email_sent.ts b/apps/sim/triggers/lemlist/email_sent.ts index f45bdf4aa..7fd38bb95 100644 --- a/apps/sim/triggers/lemlist/email_sent.ts +++ b/apps/sim/triggers/lemlist/email_sent.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildEmailSentOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistEmailSentTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_email_sent'), }), - outputs: buildActivityOutputs(), + outputs: buildEmailSentOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/interested.ts b/apps/sim/triggers/lemlist/interested.ts index e85ea40e9..a0b2b6428 100644 --- a/apps/sim/triggers/lemlist/interested.ts +++ b/apps/sim/triggers/lemlist/interested.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildInterestOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistInterestedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_interested'), }), - outputs: buildActivityOutputs(), + outputs: buildInterestOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/linkedin_replied.ts b/apps/sim/triggers/lemlist/linkedin_replied.ts index e3ccae016..267b56209 100644 --- a/apps/sim/triggers/lemlist/linkedin_replied.ts +++ b/apps/sim/triggers/lemlist/linkedin_replied.ts @@ -2,7 +2,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { buildLemlistExtraFields, - buildLinkedInReplyOutputs, + buildLinkedInRepliedOutputs, lemlistSetupInstructions, lemlistTriggerOptions, } from '@/triggers/lemlist/utils' @@ -27,7 +27,7 @@ export const lemlistLinkedInRepliedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_linkedin_replied'), }), - outputs: buildLinkedInReplyOutputs(), + outputs: buildLinkedInRepliedOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/not_interested.ts b/apps/sim/triggers/lemlist/not_interested.ts index 558142901..f53f5c512 100644 --- a/apps/sim/triggers/lemlist/not_interested.ts +++ b/apps/sim/triggers/lemlist/not_interested.ts @@ -1,7 +1,7 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildActivityOutputs, + buildInterestOutputs, buildLemlistExtraFields, lemlistSetupInstructions, lemlistTriggerOptions, @@ -27,7 +27,7 @@ export const lemlistNotInterestedTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_not_interested'), }), - outputs: buildActivityOutputs(), + outputs: buildInterestOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/lemlist/utils.ts b/apps/sim/triggers/lemlist/utils.ts index 69fbc1d4b..6ecbcce6c 100644 --- a/apps/sim/triggers/lemlist/utils.ts +++ b/apps/sim/triggers/lemlist/utils.ts @@ -66,203 +66,254 @@ export function buildLemlistExtraFields(triggerId: string) { } /** - * Base activity outputs shared across all Lemlist triggers + * Core fields present in ALL Lemlist webhook payloads + * See: https://help.lemlist.com/en/articles/9423940-use-the-api-to-list-activity-types */ -function buildBaseActivityOutputs(): Record { +const coreOutputs = { + _id: { + type: 'string', + description: 'Unique activity identifier', + }, + type: { + type: 'string', + description: 'Activity type (e.g., emailsSent, emailsReplied)', + }, + createdAt: { + type: 'string', + description: 'Activity creation timestamp (ISO 8601)', + }, + teamId: { + type: 'string', + description: 'Lemlist team identifier', + }, + leadId: { + type: 'string', + description: 'Lead identifier', + }, + campaignId: { + type: 'string', + description: 'Campaign identifier', + }, + campaignName: { + type: 'string', + description: 'Campaign name', + }, +} as const + +/** + * Lead fields present in webhook payloads + */ +const leadOutputs = { + email: { + type: 'string', + description: 'Lead email address', + }, + firstName: { + type: 'string', + description: 'Lead first name', + }, + lastName: { + type: 'string', + description: 'Lead last name', + }, + companyName: { + type: 'string', + description: 'Lead company name', + }, + linkedinUrl: { + type: 'string', + description: 'Lead LinkedIn profile URL', + }, +} as const + +/** + * Sequence/campaign tracking fields for email activities + */ +const sequenceOutputs = { + sequenceId: { + type: 'string', + description: 'Sequence identifier', + }, + sequenceStep: { + type: 'number', + description: 'Current step in the sequence (0-indexed)', + }, + totalSequenceStep: { + type: 'number', + description: 'Total number of steps in the sequence', + }, + isFirst: { + type: 'boolean', + description: 'Whether this is the first activity of this type for this step', + }, +} as const + +/** + * Sender information fields + */ +const senderOutputs = { + sendUserId: { + type: 'string', + description: 'Sender user identifier', + }, + sendUserEmail: { + type: 'string', + description: 'Sender email address', + }, + sendUserName: { + type: 'string', + description: 'Sender display name', + }, +} as const + +/** + * Email content fields + */ +const emailContentOutputs = { + subject: { + type: 'string', + description: 'Email subject line', + }, + text: { + type: 'string', + description: 'Email body content (HTML)', + }, + messageId: { + type: 'string', + description: 'Email message ID (RFC 2822 format)', + }, + emailId: { + type: 'string', + description: 'Lemlist email identifier', + }, +} as const + +/** + * Build outputs for email sent events + */ +export function buildEmailSentOutputs(): Record { return { - type: { - type: 'string', - description: 'Activity type (emailsReplied, linkedinReplied, interested, emailsOpened, etc.)', - }, - _id: { - type: 'string', - description: 'Unique activity identifier', - }, - leadId: { - type: 'string', - description: 'Associated lead ID', - }, - campaignId: { - type: 'string', - description: 'Campaign ID', - }, - campaignName: { - type: 'string', - description: 'Campaign name', - }, - sequenceId: { - type: 'string', - description: 'Sequence ID within the campaign', - }, - stepId: { - type: 'string', - description: 'Step ID that triggered this activity', - }, - createdAt: { - type: 'string', - description: 'When the activity occurred (ISO 8601)', - }, - } + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, + ...emailContentOutputs, + } as Record } /** - * Lead outputs - information about the lead + * Build outputs for email replied events */ -function buildLeadOutputs(): Record { +export function buildEmailRepliedOutputs(): Record { return { - lead: { - _id: { - type: 'string', - description: 'Lead unique identifier', - }, - email: { - type: 'string', - description: 'Lead email address', - }, - firstName: { - type: 'string', - description: 'Lead first name', - }, - lastName: { - type: 'string', - description: 'Lead last name', - }, - companyName: { - type: 'string', - description: 'Lead company name', - }, - phone: { - type: 'string', - description: 'Lead phone number', - }, - linkedinUrl: { - type: 'string', - description: 'Lead LinkedIn profile URL', - }, - picture: { - type: 'string', - description: 'Lead profile picture URL', - }, - icebreaker: { - type: 'string', - description: 'Personalized icebreaker text', - }, - timezone: { - type: 'string', - description: 'Lead timezone (e.g., America/New_York)', - }, - isUnsubscribed: { - type: 'boolean', - description: 'Whether the lead is unsubscribed', - }, - }, - } + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, + ...emailContentOutputs, + } as Record } /** - * Standard activity outputs (activity + lead data) + * Build outputs for email opened events */ -export function buildActivityOutputs(): Record { +export function buildEmailOpenedOutputs(): Record { return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), - webhook: { - type: 'json', - description: 'Full webhook payload with all activity-specific data', - }, - } -} - -/** - * Email-specific outputs (includes message content for replies) - */ -export function buildEmailReplyOutputs(): Record { - return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, messageId: { type: 'string', - description: 'Email message ID', + description: 'Email message ID that was opened', }, - subject: { - type: 'string', - description: 'Email subject line', - }, - text: { - type: 'string', - description: 'Email reply text content', - }, - html: { - type: 'string', - description: 'Email reply HTML content', - }, - sentAt: { - type: 'string', - description: 'When the reply was sent', - }, - webhook: { - type: 'json', - description: 'Full webhook payload with all email data', - }, - } + } as Record } /** - * LinkedIn-specific outputs (includes message content) + * Build outputs for email clicked events */ -export function buildLinkedInReplyOutputs(): Record { +export function buildEmailClickedOutputs(): Record { return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, messageId: { type: 'string', - description: 'LinkedIn message ID', + description: 'Email message ID containing the clicked link', }, - text: { + clickedUrl: { type: 'string', - description: 'LinkedIn message text content', + description: 'URL that was clicked', }, - sentAt: { - type: 'string', - description: 'When the message was sent', - }, - webhook: { - type: 'json', - description: 'Full webhook payload with all LinkedIn data', - }, - } + } as Record } /** - * All outputs for generic webhook (activity + lead + all possible fields) + * Build outputs for email bounced events */ -export function buildAllOutputs(): Record { +export function buildEmailBouncedOutputs(): Record { return { - ...buildBaseActivityOutputs(), - ...buildLeadOutputs(), + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, messageId: { type: 'string', - description: 'Message ID (for email/LinkedIn events)', + description: 'Email message ID that bounced', }, - subject: { + errorMessage: { type: 'string', - description: 'Email subject (for email events)', + description: 'Bounce error message', }, + } as Record +} + +/** + * Build outputs for LinkedIn replied events + */ +export function buildLinkedInRepliedOutputs(): Record { + return { + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, text: { type: 'string', - description: 'Message text content', + description: 'LinkedIn message content', }, - html: { - type: 'string', - description: 'Message HTML content (for email events)', - }, - sentAt: { - type: 'string', - description: 'When the message was sent', - }, - webhook: { - type: 'json', - description: 'Full webhook payload with all data', - }, - } + } as Record +} + +/** + * Build outputs for interested/not interested events + */ +export function buildInterestOutputs(): Record { + return { + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + } as Record +} + +/** + * Build outputs for generic webhook (all events) + * Includes all possible fields across event types + */ +export function buildLemlistOutputs(): Record { + return { + ...coreOutputs, + ...leadOutputs, + ...sequenceOutputs, + ...senderOutputs, + ...emailContentOutputs, + clickedUrl: { + type: 'string', + description: 'URL that was clicked (for emailsClicked events)', + }, + errorMessage: { + type: 'string', + description: 'Error message (for bounce/failed events)', + }, + } as Record } diff --git a/apps/sim/triggers/lemlist/webhook.ts b/apps/sim/triggers/lemlist/webhook.ts index 289d8dead..ef557b1c5 100644 --- a/apps/sim/triggers/lemlist/webhook.ts +++ b/apps/sim/triggers/lemlist/webhook.ts @@ -1,8 +1,8 @@ import { LemlistIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' import { - buildAllOutputs, buildLemlistExtraFields, + buildLemlistOutputs, lemlistSetupInstructions, lemlistTriggerOptions, } from '@/triggers/lemlist/utils' @@ -27,7 +27,7 @@ export const lemlistWebhookTrigger: TriggerConfig = { extraFields: buildLemlistExtraFields('lemlist_webhook'), }), - outputs: buildAllOutputs(), + outputs: buildLemlistOutputs(), webhook: { method: 'POST', diff --git a/apps/sim/triggers/telegram/webhook.ts b/apps/sim/triggers/telegram/webhook.ts index aad6174d6..a11aaf719 100644 --- a/apps/sim/triggers/telegram/webhook.ts +++ b/apps/sim/triggers/telegram/webhook.ts @@ -110,6 +110,7 @@ export const telegramWebhookTrigger: TriggerConfig = { }, sender: { id: { type: 'number', description: 'Sender user ID' }, + username: { type: 'string', description: 'Sender username (if available)' }, firstName: { type: 'string', description: 'Sender first name' }, lastName: { type: 'string', description: 'Sender last name' }, languageCode: { type: 'string', description: 'Sender language code (if available)' }, diff --git a/apps/sim/triggers/typeform/webhook.ts b/apps/sim/triggers/typeform/webhook.ts index 90d5069fc..b846a0539 100644 --- a/apps/sim/triggers/typeform/webhook.ts +++ b/apps/sim/triggers/typeform/webhook.ts @@ -136,6 +136,8 @@ export const typeformWebhookTrigger: TriggerConfig = { 'Array of respondent answers (only includes answered questions). Each answer contains type, value, and field reference.', }, definition: { + description: + 'Form definition (only included when "Include Form Definition" is enabled in trigger settings)', id: { type: 'string', description: 'Form ID', diff --git a/apps/sim/triggers/webflow/collection_item_changed.ts b/apps/sim/triggers/webflow/collection_item_changed.ts index e66590f5c..aedeae63a 100644 --- a/apps/sim/triggers/webflow/collection_item_changed.ts +++ b/apps/sim/triggers/webflow/collection_item_changed.ts @@ -96,10 +96,6 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the event occurred', }, - workspaceId: { - type: 'string', - description: 'The workspace ID where the event occurred', - }, collectionId: { type: 'string', description: 'The collection ID where the item was changed', diff --git a/apps/sim/triggers/webflow/collection_item_created.ts b/apps/sim/triggers/webflow/collection_item_created.ts index e0fdc7e8a..777b74b76 100644 --- a/apps/sim/triggers/webflow/collection_item_created.ts +++ b/apps/sim/triggers/webflow/collection_item_created.ts @@ -109,10 +109,6 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the event occurred', }, - workspaceId: { - type: 'string', - description: 'The workspace ID where the event occurred', - }, collectionId: { type: 'string', description: 'The collection ID where the item was created', diff --git a/apps/sim/triggers/webflow/collection_item_deleted.ts b/apps/sim/triggers/webflow/collection_item_deleted.ts index 12a10f3ed..60fb2805d 100644 --- a/apps/sim/triggers/webflow/collection_item_deleted.ts +++ b/apps/sim/triggers/webflow/collection_item_deleted.ts @@ -97,10 +97,6 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the event occurred', }, - workspaceId: { - type: 'string', - description: 'The workspace ID where the event occurred', - }, collectionId: { type: 'string', description: 'The collection ID where the item was deleted', diff --git a/apps/sim/triggers/webflow/form_submission.ts b/apps/sim/triggers/webflow/form_submission.ts index 7c27599c6..1a6c7640b 100644 --- a/apps/sim/triggers/webflow/form_submission.ts +++ b/apps/sim/triggers/webflow/form_submission.ts @@ -76,9 +76,9 @@ export const webflowFormSubmissionTrigger: TriggerConfig = { type: 'string', description: 'The site ID where the form was submitted', }, - workspaceId: { + formId: { type: 'string', - description: 'The workspace ID where the event occurred', + description: 'The form ID', }, name: { type: 'string', diff --git a/bun.lock b/bun.lock index 8b136165e..c7cee5691 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio",