Compare commits

...

6 Commits

Author SHA1 Message Date
Vikhyath Mondreti
df20d1035b remove branch from unrelated ops 2026-01-13 17:47:32 -08:00
Vikhyath Mondreti
d9d261de1d remove branch field for ones where it's not relevant 2026-01-13 17:46:05 -08:00
Vikhyath Mondreti
2cb4593d81 more test fixes 2026-01-13 17:17:23 -08:00
Vikhyath Mondreti
f04a7b5364 fix tests 2026-01-13 17:16:35 -08:00
Vikhyath Mondreti
00fb84fbde cleanup trigger outputs 2026-01-13 17:14:37 -08:00
Vikhyath Mondreti
ce38024b20 fix(triggers): package lemlist data, cleanup trigger outputs formatting, fix display name issues 2026-01-13 16:23:46 -08:00
38 changed files with 553 additions and 989 deletions

View File

@@ -552,6 +552,53 @@ All fields automatically have:
- `mode: 'trigger'` - Only shown in trigger mode - `mode: 'trigger'` - Only shown in trigger mode
- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected - `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
Trigger outputs use the same schema as block outputs (NOT tool 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 `delete{Service}Webhook` function to `provider-subscriptions.ts`
- [ ] Added provider to `cleanupExternalWebhook` function - [ ] 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 ### Testing
- [ ] Run `bun run type-check` to verify no TypeScript errors - [ ] Run `bun run type-check` to verify no TypeScript errors
- [ ] Restart dev server to pick up new triggers - [ ] Restart dev server to pick up new triggers

View File

@@ -313,6 +313,26 @@ export const getBlock = (type: string): BlockConfig | undefined => {
return registry[normalized] 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 => { export const getBlockByToolName = (toolName: string): BlockConfig | undefined => {
return Object.values(registry).find((block) => block.tools?.access?.includes(toolName)) return Object.values(registry).find((block) => block.tools?.access?.includes(toolName))
} }

View File

@@ -378,21 +378,10 @@ function buildManualTriggerOutput(
} }
function buildIntegrationTriggerOutput( function buildIntegrationTriggerOutput(
finalInput: unknown, _finalInput: unknown,
workflowInput: unknown workflowInput: unknown
): NormalizedBlockOutput { ): NormalizedBlockOutput {
const base: NormalizedBlockOutput = isPlainObject(workflowInput) return isPlainObject(workflowInput) ? (workflowInput as NormalizedBlockOutput) : {}
? ({ ...(workflowInput as Record<string, unknown>) } as NormalizedBlockOutput)
: {}
if (isPlainObject(finalInput)) {
Object.assign(base, finalInput as Record<string, unknown>)
base.input = { ...(finalInput as Record<string, unknown>) }
} else {
base.input = finalInput
}
return mergeFilesIntoOutput(base, workflowInput)
} }
function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | undefined { function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | undefined {

View File

@@ -1,4 +1,4 @@
import { getBlock } from '@/blocks/registry' import { getLatestBlock } from '@/blocks/registry'
import { getAllTriggers } from '@/triggers' import { getAllTriggers } from '@/triggers'
export interface TriggerOption { export interface TriggerOption {
@@ -48,22 +48,13 @@ export function getTriggerOptions(): TriggerOption[] {
continue continue
} }
const block = getBlock(provider) const block = getLatestBlock(provider)
if (block) { providerMap.set(provider, {
providerMap.set(provider, { value: provider,
value: provider, label: block?.name || formatProviderName(provider),
label: block.name, // Use block's display name (e.g., "Slack", "GitHub") color: block?.bgColor || '#6b7280',
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
})
}
} }
const integrationOptions = Array.from(providerMap.values()).sort((a, b) => const integrationOptions = Array.from(providerMap.values()).sort((a, b) =>

File diff suppressed because it is too large Load Diff

View File

@@ -2290,7 +2290,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
webhookId: { value: null }, webhookId: { value: null },
}, },
}), }),
@@ -2302,7 +2302,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
webhookId: { value: 'wh_123456' }, webhookId: { value: 'wh_123456' },
}, },
}), }),
@@ -2318,7 +2318,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
triggerPath: { value: '' }, triggerPath: { value: '' },
}, },
}), }),
@@ -2330,7 +2330,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
triggerPath: { value: '/api/webhooks/abc123' }, triggerPath: { value: '/api/webhooks/abc123' },
}, },
}), }),
@@ -2346,7 +2346,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
webhookId: { value: null }, webhookId: { value: null },
triggerPath: { value: '' }, triggerPath: { value: '' },
}, },
@@ -2359,7 +2359,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
webhookId: { value: 'wh_123456' }, webhookId: { value: 'wh_123456' },
triggerPath: { value: '/api/webhooks/abc123' }, triggerPath: { value: '/api/webhooks/abc123' },
}, },
@@ -2371,14 +2371,18 @@ describe('hasWorkflowChanged', () => {
}) })
it.concurrent( 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({ const deployedState = createWorkflowState({
blocks: { blocks: {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
webhookId: { value: null }, webhookId: { value: null },
}, },
}), }),
@@ -2390,7 +2394,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'pull_request' } }, model: { value: 'gpt-4o' },
webhookId: { value: 'wh_123456' }, webhookId: { value: 'wh_123456' },
}, },
}), }),
@@ -2402,8 +2406,12 @@ describe('hasWorkflowChanged', () => {
) )
it.concurrent( 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({ const deployedState = createWorkflowState({
blocks: { blocks: {
block1: createBlock('block1', { block1: createBlock('block1', {
@@ -2420,7 +2428,36 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { 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' }, webhookId: { value: 'wh_123456' },
triggerPath: { value: '/api/webhooks/abc123' }, triggerPath: { value: '/api/webhooks/abc123' },
}, },
@@ -2440,7 +2477,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
webhookId: { value: 'wh_old123' }, webhookId: { value: 'wh_old123' },
triggerPath: { value: '/api/webhooks/old' }, triggerPath: { value: '/api/webhooks/old' },
}, },
@@ -2453,7 +2490,7 @@ describe('hasWorkflowChanged', () => {
block1: createBlock('block1', { block1: createBlock('block1', {
type: 'starter', type: 'starter',
subBlocks: { subBlocks: {
triggerConfig: { value: { event: 'push' } }, model: { value: 'gpt-4' },
}, },
}), }),
}, },

View File

@@ -96,23 +96,3 @@ export function buildMeetingOutputs(): Record<string, TriggerOutput> {
}, },
} as Record<string, TriggerOutput> } as Record<string, TriggerOutput>
} }
/**
* Build output schema for generic webhook events
*/
export function buildGenericOutputs(): Record<string, TriggerOutput> {
return {
payload: {
type: 'object',
description: 'Raw webhook payload',
},
headers: {
type: 'object',
description: 'Request headers',
},
timestamp: {
type: 'string',
description: 'ISO8601 received timestamp',
},
} as Record<string, TriggerOutput>
}

View File

@@ -1,6 +1,6 @@
import { CirclebackIcon } from '@/components/icons' import { CirclebackIcon } from '@/components/icons'
import type { TriggerConfig } from '@/triggers/types' import type { TriggerConfig } from '@/triggers/types'
import { buildGenericOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils' import { buildMeetingOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils'
export const circlebackWebhookTrigger: TriggerConfig = { export const circlebackWebhookTrigger: TriggerConfig = {
id: 'circleback_webhook', id: 'circleback_webhook',
@@ -74,7 +74,7 @@ export const circlebackWebhookTrigger: TriggerConfig = {
}, },
], ],
outputs: buildGenericOutputs(), outputs: buildMeetingOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -31,8 +31,14 @@ export const TRIGGER_PERSISTED_SUBBLOCK_IDS: string[] = [
/** /**
* Trigger-related subblock IDs that represent runtime metadata. They should remain * 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. * 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. * Maximum number of consecutive failures before a trigger (schedule/webhook) is auto-disabled.

View File

@@ -116,6 +116,11 @@ export const githubIssueClosedTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description:
'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (opened, closed, reopened, edited, etc.)', description: 'Action performed (opened, closed, reopened, edited, etc.)',

View File

@@ -117,6 +117,10 @@ export const githubIssueCommentTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (created, edited, deleted)', description: 'Action performed (created, edited, deleted)',

View File

@@ -137,6 +137,11 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description:
'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (opened, closed, reopened, edited, etc.)', description: 'Action performed (opened, closed, reopened, edited, etc.)',

View File

@@ -117,6 +117,10 @@ export const githubPRClosedTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)',

View File

@@ -117,6 +117,10 @@ export const githubPRCommentTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (created, edited, deleted)', description: 'Action performed (created, edited, deleted)',

View File

@@ -116,6 +116,10 @@ export const githubPRMergedTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)',

View File

@@ -116,6 +116,10 @@ export const githubPROpenedTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)', description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)',

View File

@@ -117,6 +117,10 @@ export const githubPRReviewedTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request_review)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (submitted, edited, dismissed)', description: 'Action performed (submitted, edited, dismissed)',

View File

@@ -116,6 +116,14 @@ export const githubPushTrigger: TriggerConfig = {
], ],
outputs: { 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: { ref: {
type: 'string', type: 'string',
description: 'Git reference that was pushed (e.g., refs/heads/main)', description: 'Git reference that was pushed (e.g., refs/heads/main)',

View File

@@ -116,6 +116,10 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., release)',
},
action: { action: {
type: 'string', type: 'string',
description: description:

View File

@@ -117,6 +117,10 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
], ],
outputs: { outputs: {
event_type: {
type: 'string',
description: 'GitHub event type from X-GitHub-Event header (e.g., workflow_run)',
},
action: { action: {
type: 'string', type: 'string',
description: 'Action performed (requested, in_progress, completed)', description: 'Action performed (requested, in_progress, completed)',

View File

@@ -265,11 +265,6 @@ function buildBaseWebhookOutputs(): Record<string, TriggerOutput> {
}, },
}, },
}, },
webhook: {
type: 'json',
description: 'Webhook metadata including provider, path, and raw payload',
},
} }
} }

View File

@@ -1,7 +1,7 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildActivityOutputs, buildEmailBouncedOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
@@ -27,7 +27,7 @@ export const lemlistEmailBouncedTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_email_bounced'), extraFields: buildLemlistExtraFields('lemlist_email_bounced'),
}), }),
outputs: buildActivityOutputs(), outputs: buildEmailBouncedOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -1,7 +1,7 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildActivityOutputs, buildEmailClickedOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
@@ -27,7 +27,7 @@ export const lemlistEmailClickedTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_email_clicked'), extraFields: buildLemlistExtraFields('lemlist_email_clicked'),
}), }),
outputs: buildActivityOutputs(), outputs: buildEmailClickedOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -1,7 +1,7 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildActivityOutputs, buildEmailOpenedOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
@@ -27,7 +27,7 @@ export const lemlistEmailOpenedTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_email_opened'), extraFields: buildLemlistExtraFields('lemlist_email_opened'),
}), }),
outputs: buildActivityOutputs(), outputs: buildEmailOpenedOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -1,7 +1,7 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildEmailReplyOutputs, buildEmailRepliedOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
@@ -30,7 +30,7 @@ export const lemlistEmailRepliedTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_email_replied'), extraFields: buildLemlistExtraFields('lemlist_email_replied'),
}), }),
outputs: buildEmailReplyOutputs(), outputs: buildEmailRepliedOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -1,7 +1,7 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildActivityOutputs, buildEmailSentOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
@@ -27,7 +27,7 @@ export const lemlistEmailSentTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_email_sent'), extraFields: buildLemlistExtraFields('lemlist_email_sent'),
}), }),
outputs: buildActivityOutputs(), outputs: buildEmailSentOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -1,7 +1,7 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildActivityOutputs, buildInterestOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
@@ -27,7 +27,7 @@ export const lemlistInterestedTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_interested'), extraFields: buildLemlistExtraFields('lemlist_interested'),
}), }),
outputs: buildActivityOutputs(), outputs: buildInterestOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -2,7 +2,7 @@ import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildLemlistExtraFields, buildLemlistExtraFields,
buildLinkedInReplyOutputs, buildLinkedInRepliedOutputs,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
} from '@/triggers/lemlist/utils' } from '@/triggers/lemlist/utils'
@@ -27,7 +27,7 @@ export const lemlistLinkedInRepliedTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_linkedin_replied'), extraFields: buildLemlistExtraFields('lemlist_linkedin_replied'),
}), }),
outputs: buildLinkedInReplyOutputs(), outputs: buildLinkedInRepliedOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -1,7 +1,7 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildActivityOutputs, buildInterestOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
@@ -27,7 +27,7 @@ export const lemlistNotInterestedTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_not_interested'), extraFields: buildLemlistExtraFields('lemlist_not_interested'),
}), }),
outputs: buildActivityOutputs(), outputs: buildInterestOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -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<string, TriggerOutput> { 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<string, TriggerOutput> {
return { return {
type: { ...coreOutputs,
type: 'string', ...leadOutputs,
description: 'Activity type (emailsReplied, linkedinReplied, interested, emailsOpened, etc.)', ...sequenceOutputs,
}, ...senderOutputs,
_id: { ...emailContentOutputs,
type: 'string', } as Record<string, TriggerOutput>
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)',
},
}
} }
/** /**
* Lead outputs - information about the lead * Build outputs for email replied events
*/ */
function buildLeadOutputs(): Record<string, TriggerOutput> { export function buildEmailRepliedOutputs(): Record<string, TriggerOutput> {
return { return {
lead: { ...coreOutputs,
_id: { ...leadOutputs,
type: 'string', ...sequenceOutputs,
description: 'Lead unique identifier', ...senderOutputs,
}, ...emailContentOutputs,
email: { } as Record<string, TriggerOutput>
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',
},
},
}
} }
/** /**
* Standard activity outputs (activity + lead data) * Build outputs for email opened events
*/ */
export function buildActivityOutputs(): Record<string, TriggerOutput> { export function buildEmailOpenedOutputs(): Record<string, TriggerOutput> {
return { return {
...buildBaseActivityOutputs(), ...coreOutputs,
...buildLeadOutputs(), ...leadOutputs,
webhook: { ...sequenceOutputs,
type: 'json', ...senderOutputs,
description: 'Full webhook payload with all activity-specific data',
},
}
}
/**
* Email-specific outputs (includes message content for replies)
*/
export function buildEmailReplyOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseActivityOutputs(),
...buildLeadOutputs(),
messageId: { messageId: {
type: 'string', type: 'string',
description: 'Email message ID', description: 'Email message ID that was opened',
}, },
subject: { } as Record<string, TriggerOutput>
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',
},
}
} }
/** /**
* LinkedIn-specific outputs (includes message content) * Build outputs for email clicked events
*/ */
export function buildLinkedInReplyOutputs(): Record<string, TriggerOutput> { export function buildEmailClickedOutputs(): Record<string, TriggerOutput> {
return { return {
...buildBaseActivityOutputs(), ...coreOutputs,
...buildLeadOutputs(), ...leadOutputs,
...sequenceOutputs,
...senderOutputs,
messageId: { messageId: {
type: 'string', type: 'string',
description: 'LinkedIn message ID', description: 'Email message ID containing the clicked link',
}, },
text: { clickedUrl: {
type: 'string', type: 'string',
description: 'LinkedIn message text content', description: 'URL that was clicked',
}, },
sentAt: { } as Record<string, TriggerOutput>
type: 'string',
description: 'When the message was sent',
},
webhook: {
type: 'json',
description: 'Full webhook payload with all LinkedIn data',
},
}
} }
/** /**
* All outputs for generic webhook (activity + lead + all possible fields) * Build outputs for email bounced events
*/ */
export function buildAllOutputs(): Record<string, TriggerOutput> { export function buildEmailBouncedOutputs(): Record<string, TriggerOutput> {
return { return {
...buildBaseActivityOutputs(), ...coreOutputs,
...buildLeadOutputs(), ...leadOutputs,
...sequenceOutputs,
...senderOutputs,
messageId: { messageId: {
type: 'string', type: 'string',
description: 'Message ID (for email/LinkedIn events)', description: 'Email message ID that bounced',
}, },
subject: { errorMessage: {
type: 'string', type: 'string',
description: 'Email subject (for email events)', description: 'Bounce error message',
}, },
} as Record<string, TriggerOutput>
}
/**
* Build outputs for LinkedIn replied events
*/
export function buildLinkedInRepliedOutputs(): Record<string, TriggerOutput> {
return {
...coreOutputs,
...leadOutputs,
...sequenceOutputs,
text: { text: {
type: 'string', type: 'string',
description: 'Message text content', description: 'LinkedIn message content',
}, },
html: { } as Record<string, TriggerOutput>
type: 'string', }
description: 'Message HTML content (for email events)',
}, /**
sentAt: { * Build outputs for interested/not interested events
type: 'string', */
description: 'When the message was sent', export function buildInterestOutputs(): Record<string, TriggerOutput> {
}, return {
webhook: { ...coreOutputs,
type: 'json', ...leadOutputs,
description: 'Full webhook payload with all data', ...sequenceOutputs,
}, } as Record<string, TriggerOutput>
} }
/**
* Build outputs for generic webhook (all events)
* Includes all possible fields across event types
*/
export function buildLemlistOutputs(): Record<string, TriggerOutput> {
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<string, TriggerOutput>
} }

View File

@@ -1,8 +1,8 @@
import { LemlistIcon } from '@/components/icons' import { LemlistIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers' import { buildTriggerSubBlocks } from '@/triggers'
import { import {
buildAllOutputs,
buildLemlistExtraFields, buildLemlistExtraFields,
buildLemlistOutputs,
lemlistSetupInstructions, lemlistSetupInstructions,
lemlistTriggerOptions, lemlistTriggerOptions,
} from '@/triggers/lemlist/utils' } from '@/triggers/lemlist/utils'
@@ -27,7 +27,7 @@ export const lemlistWebhookTrigger: TriggerConfig = {
extraFields: buildLemlistExtraFields('lemlist_webhook'), extraFields: buildLemlistExtraFields('lemlist_webhook'),
}), }),
outputs: buildAllOutputs(), outputs: buildLemlistOutputs(),
webhook: { webhook: {
method: 'POST', method: 'POST',

View File

@@ -110,6 +110,7 @@ export const telegramWebhookTrigger: TriggerConfig = {
}, },
sender: { sender: {
id: { type: 'number', description: 'Sender user ID' }, id: { type: 'number', description: 'Sender user ID' },
username: { type: 'string', description: 'Sender username (if available)' },
firstName: { type: 'string', description: 'Sender first name' }, firstName: { type: 'string', description: 'Sender first name' },
lastName: { type: 'string', description: 'Sender last name' }, lastName: { type: 'string', description: 'Sender last name' },
languageCode: { type: 'string', description: 'Sender language code (if available)' }, languageCode: { type: 'string', description: 'Sender language code (if available)' },

View File

@@ -136,6 +136,8 @@ export const typeformWebhookTrigger: TriggerConfig = {
'Array of respondent answers (only includes answered questions). Each answer contains type, value, and field reference.', 'Array of respondent answers (only includes answered questions). Each answer contains type, value, and field reference.',
}, },
definition: { definition: {
description:
'Form definition (only included when "Include Form Definition" is enabled in trigger settings)',
id: { id: {
type: 'string', type: 'string',
description: 'Form ID', description: 'Form ID',

View File

@@ -96,10 +96,6 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
type: 'string', type: 'string',
description: 'The site ID where the event occurred', description: 'The site ID where the event occurred',
}, },
workspaceId: {
type: 'string',
description: 'The workspace ID where the event occurred',
},
collectionId: { collectionId: {
type: 'string', type: 'string',
description: 'The collection ID where the item was changed', description: 'The collection ID where the item was changed',

View File

@@ -109,10 +109,6 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
type: 'string', type: 'string',
description: 'The site ID where the event occurred', description: 'The site ID where the event occurred',
}, },
workspaceId: {
type: 'string',
description: 'The workspace ID where the event occurred',
},
collectionId: { collectionId: {
type: 'string', type: 'string',
description: 'The collection ID where the item was created', description: 'The collection ID where the item was created',

View File

@@ -97,10 +97,6 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
type: 'string', type: 'string',
description: 'The site ID where the event occurred', description: 'The site ID where the event occurred',
}, },
workspaceId: {
type: 'string',
description: 'The workspace ID where the event occurred',
},
collectionId: { collectionId: {
type: 'string', type: 'string',
description: 'The collection ID where the item was deleted', description: 'The collection ID where the item was deleted',

View File

@@ -76,9 +76,9 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
type: 'string', type: 'string',
description: 'The site ID where the form was submitted', description: 'The site ID where the form was submitted',
}, },
workspaceId: { formId: {
type: 'string', type: 'string',
description: 'The workspace ID where the event occurred', description: 'The form ID',
}, },
name: { name: {
type: 'string', type: 'string',

View File

@@ -1,6 +1,5 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "simstudio", "name": "simstudio",