diff --git a/.agents/skills/add-trigger/SKILL.md b/.agents/skills/add-trigger/SKILL.md
index fbf27ef625d..fd6df46e505 100644
--- a/.agents/skills/add-trigger/SKILL.md
+++ b/.agents/skills/add-trigger/SKILL.md
@@ -3,63 +3,57 @@ name: add-trigger
description: Create or update Sim webhook triggers using the generic trigger builder, service-specific setup instructions, outputs, and registry wiring. Use when working in `apps/sim/triggers/{service}/` or adding webhook support to an integration.
---
-# Add Trigger Skill
+# Add Trigger
You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
## Your Task
-When the user asks you to create triggers for a service:
1. Research what webhook events the service supports
2. Create the trigger files using the generic builder
-3. Register triggers and connect them to the block
+3. Create a provider handler if custom auth, formatting, or subscriptions are needed
+4. Register triggers and connect them to the block
## Directory Structure
```
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
-├── utils.ts # Service-specific helpers (trigger options, setup instructions, extra fields)
+├── utils.ts # Service-specific helpers (options, instructions, extra fields, outputs)
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.ts # Secondary trigger (no dropdown)
-├── {event_c}.ts # Secondary trigger (no dropdown)
└── webhook.ts # Generic webhook trigger (optional, for "all events")
+
+apps/sim/lib/webhooks/
+├── provider-subscription-utils.ts # Shared subscription helpers (getProviderConfig, getNotificationUrl)
+├── providers/
+│ ├── {service}.ts # Provider handler (auth, formatInput, matchEvent, subscriptions)
+│ ├── types.ts # WebhookProviderHandler interface
+│ ├── utils.ts # Shared helpers (createHmacVerifier, verifyTokenAuth, skipByEventTypes)
+│ └── registry.ts # Handler map + default handler
```
-## Step 1: Create utils.ts
+## Step 1: Create `utils.ts`
-This file contains service-specific helpers used by all triggers.
+This file contains all service-specific helpers used by triggers.
```typescript
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
-/**
- * Dropdown options for the trigger type selector.
- * These appear in the primary trigger's dropdown.
- */
export const {service}TriggerOptions = [
{ label: 'Event A', id: '{service}_event_a' },
{ label: 'Event B', id: '{service}_event_b' },
- { label: 'Event C', id: '{service}_event_c' },
- { label: 'Generic Webhook (All Events)', id: '{service}_webhook' },
]
-/**
- * Generates HTML setup instructions for the trigger.
- * Displayed to users to help them configure webhooks in the external service.
- */
export function {service}SetupInstructions(eventType: string): string {
const instructions = [
'Copy the Webhook URL above',
'Go to {Service} Settings > Webhooks',
- 'Click Add Webhook',
- 'Paste the webhook URL',
`Select the ${eventType} event type`,
- 'Save the webhook configuration',
+ 'Paste the webhook URL and save',
'Click "Save" above to activate your trigger',
]
-
return instructions
.map((instruction, index) =>
`
${index + 1}. ${instruction}
`
@@ -67,10 +61,6 @@ export function {service}SetupInstructions(eventType: string): string {
.join('')
}
-/**
- * Service-specific extra fields to add to triggers.
- * These are inserted between webhookUrl and triggerSave.
- */
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
@@ -78,53 +68,34 @@ export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
title: 'Project ID (Optional)',
type: 'short-input',
placeholder: 'Leave empty for all projects',
- description: 'Optionally filter to a specific project',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
-/**
- * Build outputs for this trigger type.
- * Outputs define what data is available to downstream blocks.
- */
export function build{Service}Outputs(): Record {
return {
- eventType: { type: 'string', description: 'The type of event that triggered this workflow' },
+ eventType: { type: 'string', description: 'The type of event' },
resourceId: { type: 'string', description: 'ID of the affected resource' },
- timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
- // Nested outputs for complex data
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
- status: { type: 'string', description: 'Current status' },
},
- webhook: { type: 'json', description: 'Full webhook payload' },
}
}
```
-## Step 2: Create the Primary Trigger
+## Step 2: Create Trigger Files
-The **primary trigger** is the first one listed. It MUST include `includeDropdown: true` so users can switch between trigger types.
+**Primary trigger** — MUST include `includeDropdown: true`:
```typescript
import { {Service}Icon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
-import {
- build{Service}ExtraFields,
- build{Service}Outputs,
- {service}SetupInstructions,
- {service}TriggerOptions,
-} from '@/triggers/{service}/utils'
+import { build{Service}ExtraFields, build{Service}Outputs, {service}SetupInstructions, {service}TriggerOptions } from '@/triggers/{service}/utils'
import type { TriggerConfig } from '@/triggers/types'
-/**
- * {Service} Event A Trigger
- *
- * This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
- */
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
name: '{Service} Event A',
@@ -132,496 +103,222 @@ export const {service}EventATrigger: TriggerConfig = {
description: 'Trigger workflow when Event A occurs',
version: '1.0.0',
icon: {Service}Icon,
-
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_a',
triggerOptions: {service}TriggerOptions,
- includeDropdown: true, // PRIMARY TRIGGER - includes dropdown
+ includeDropdown: true,
setupInstructions: {service}SetupInstructions('Event A'),
extraFields: build{Service}ExtraFields('{service}_event_a'),
}),
-
outputs: build{Service}Outputs(),
-
- webhook: {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- },
+ webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } },
}
```
-## Step 3: Create Secondary Triggers
-
-Secondary triggers do NOT include the dropdown (it's already in the primary trigger).
+**Secondary triggers** — NO `includeDropdown` (it's already in the primary):
```typescript
-import { {Service}Icon } from '@/components/icons'
-import { buildTriggerSubBlocks } from '@/triggers'
-import {
- build{Service}ExtraFields,
- build{Service}Outputs,
- {service}SetupInstructions,
- {service}TriggerOptions,
-} from '@/triggers/{service}/utils'
-import type { TriggerConfig } from '@/triggers/types'
-
-/**
- * {Service} Event B Trigger
- */
export const {service}EventBTrigger: TriggerConfig = {
- id: '{service}_event_b',
- name: '{Service} Event B',
- provider: '{service}',
- description: 'Trigger workflow when Event B occurs',
- version: '1.0.0',
- icon: {Service}Icon,
-
- subBlocks: buildTriggerSubBlocks({
- triggerId: '{service}_event_b',
- triggerOptions: {service}TriggerOptions,
- // NO includeDropdown - secondary trigger
- setupInstructions: {service}SetupInstructions('Event B'),
- extraFields: build{Service}ExtraFields('{service}_event_b'),
- }),
-
- outputs: build{Service}Outputs(),
-
- webhook: {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- },
+ // Same as above but: id: '{service}_event_b', no includeDropdown
}
```
-## Step 4: Create index.ts Barrel Export
+## Step 3: Register and Wire
+
+### `apps/sim/triggers/{service}/index.ts`
```typescript
export { {service}EventATrigger } from './event_a'
export { {service}EventBTrigger } from './event_b'
-export { {service}EventCTrigger } from './event_c'
-export { {service}WebhookTrigger } from './webhook'
```
-## Step 5: Register Triggers
-
-### Trigger Registry (`apps/sim/triggers/registry.ts`)
+### `apps/sim/triggers/registry.ts`
```typescript
-// Add import
-import {
- {service}EventATrigger,
- {service}EventBTrigger,
- {service}EventCTrigger,
- {service}WebhookTrigger,
-} from '@/triggers/{service}'
-
-// Add to TRIGGER_REGISTRY
+import { {service}EventATrigger, {service}EventBTrigger } from '@/triggers/{service}'
+
export const TRIGGER_REGISTRY: TriggerRegistry = {
- // ... existing triggers ...
+ // ... existing ...
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
- {service}_event_c: {service}EventCTrigger,
- {service}_webhook: {service}WebhookTrigger,
}
```
-## Step 6: Connect Triggers to Block
-
-In the block file (`apps/sim/blocks/blocks/{service}.ts`):
+### Block file (`apps/sim/blocks/blocks/{service}.ts`)
```typescript
-import { {Service}Icon } from '@/components/icons'
import { getTrigger } from '@/triggers'
-import type { BlockConfig } from '@/blocks/types'
export const {Service}Block: BlockConfig = {
- type: '{service}',
- name: '{Service}',
- // ... other config ...
-
- // Enable triggers and list available trigger IDs
+ // ...
triggers: {
enabled: true,
- available: [
- '{service}_event_a',
- '{service}_event_b',
- '{service}_event_c',
- '{service}_webhook',
- ],
+ available: ['{service}_event_a', '{service}_event_b'],
},
-
subBlocks: [
- // Regular tool subBlocks first
- { id: 'operation', /* ... */ },
- { id: 'credential', /* ... */ },
- // ... other tool fields ...
-
- // Then spread ALL trigger subBlocks
+ // Regular tool subBlocks first...
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
- ...getTrigger('{service}_event_c').subBlocks,
- ...getTrigger('{service}_webhook').subBlocks,
],
-
- // ... tools config ...
}
```
-## Automatic Webhook Registration (Preferred)
-
-If the service's API supports programmatic webhook creation, implement automatic webhook registration instead of requiring users to manually configure webhooks. This provides a much better user experience.
+## Provider Handler
-### When to Use Automatic Registration
+All provider-specific webhook logic lives in a single handler file: `apps/sim/lib/webhooks/providers/{service}.ts`.
-Check the service's API documentation for endpoints like:
-- `POST /webhooks` or `POST /hooks` - Create webhook
-- `DELETE /webhooks/{id}` - Delete webhook
+### When to Create a Handler
-Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, etc.
+| Behavior | Method | Examples |
+|---|---|---|
+| HMAC signature auth | `verifyAuth` via `createHmacVerifier` | Ashby, Jira, Linear, Typeform |
+| Custom token auth | `verifyAuth` via `verifyTokenAuth` | Generic, Google Forms |
+| Event filtering | `matchEvent` | GitHub, Jira, Attio, HubSpot |
+| Idempotency dedup | `extractIdempotencyId` | Slack, Stripe, Linear, Jira |
+| Custom input formatting | `formatInput` | Slack, Teams, Attio, Ashby |
+| Auto webhook creation | `createSubscription` | Ashby, Grain, Calendly, Airtable |
+| Auto webhook deletion | `deleteSubscription` | Ashby, Grain, Calendly, Airtable |
+| Challenge/verification | `handleChallenge` | Slack, WhatsApp, Teams |
+| Custom success response | `formatSuccessResponse` | Slack, Twilio Voice, Teams |
-### Implementation Steps
+If none apply, you don't need a handler. The default handler provides bearer token auth.
-#### 1. Add API Key to Extra Fields
-
-Update your `build{Service}ExtraFields` function to include an API key field:
+### Example Handler
```typescript
-export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
- return [
- {
- id: 'apiKey',
- title: 'API Key',
- type: 'short-input',
- placeholder: 'Enter your {Service} API key',
- description: 'Required to create the webhook in {Service}.',
- password: true,
- required: true,
- mode: 'trigger',
- condition: { field: 'selectedTriggerId', value: triggerId },
- },
- // Other optional fields (e.g., campaign filter, project filter)
- {
- id: 'projectId',
- title: 'Project ID (Optional)',
- type: 'short-input',
- placeholder: 'Leave empty for all projects',
- mode: 'trigger',
- condition: { field: 'selectedTriggerId', value: triggerId },
- },
- ]
+import crypto from 'crypto'
+import { createLogger } from '@sim/logger'
+import { safeCompare } from '@/lib/core/security/encryption'
+import type { EventMatchContext, FormatInputContext, FormatInputResult, WebhookProviderHandler } from '@/lib/webhooks/providers/types'
+import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
+
+const logger = createLogger('WebhookProvider:{Service}')
+
+function validate{Service}Signature(secret: string, signature: string, body: string): boolean {
+ if (!secret || !signature || !body) return false
+ const computed = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
+ return safeCompare(computed, signature)
}
-```
-
-#### 2. Update Setup Instructions for Automatic Creation
-
-Change instructions to indicate automatic webhook creation:
-
-```typescript
-export function {service}SetupInstructions(eventType: string): string {
- const instructions = [
- 'Enter your {Service} API Key above.',
- 'You can find your API key in {Service} at Settings > API.',
- `Click "Save Configuration" to automatically create the webhook in {Service} for ${eventType} events.`,
- 'The webhook will be automatically deleted when you remove this trigger.',
- ]
- return instructions
- .map((instruction, index) =>
- `
${index + 1}. ${instruction}
`
- )
- .join('')
-}
-```
-
-#### 3. Add Webhook Creation to API Route
-
-In `apps/sim/app/api/webhooks/route.ts`, add provider-specific logic after the database save:
-
-```typescript
-// --- {Service} specific logic ---
-if (savedWebhook && provider === '{service}') {
- logger.info(`[${requestId}] {Service} provider detected. Creating webhook subscription.`)
- try {
- const result = await create{Service}WebhookSubscription(
- {
- id: savedWebhook.id,
- path: savedWebhook.path,
- providerConfig: savedWebhook.providerConfig,
- },
- requestId
- )
-
- if (result) {
- // Update the webhook record with the external webhook ID
- const updatedConfig = {
- ...(savedWebhook.providerConfig as Record),
- externalId: result.id,
- }
- await db
- .update(webhook)
- .set({
- providerConfig: updatedConfig,
- updatedAt: new Date(),
- })
- .where(eq(webhook.id, savedWebhook.id))
-
- savedWebhook.providerConfig = updatedConfig
- logger.info(`[${requestId}] Successfully created {Service} webhook`, {
- externalHookId: result.id,
- webhookId: savedWebhook.id,
- })
- }
- } catch (err) {
- logger.error(
- `[${requestId}] Error creating {Service} webhook subscription, rolling back webhook`,
- err
- )
- await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
- return NextResponse.json(
- {
- error: 'Failed to create webhook in {Service}',
- details: err instanceof Error ? err.message : 'Unknown error',
- },
- { status: 500 }
- )
- }
-}
-// --- End {Service} specific logic ---
-```
-
-Then add the helper function at the end of the file:
-
-```typescript
-async function create{Service}WebhookSubscription(
- webhookData: any,
- requestId: string
-): Promise<{ id: string } | undefined> {
- try {
- const { path, providerConfig } = webhookData
- const { apiKey, triggerId, projectId } = providerConfig || {}
-
- if (!apiKey) {
- throw new Error('{Service} API Key is required.')
- }
-
- // Map trigger IDs to service event types
- const eventTypeMap: Record = {
- {service}_event_a: 'eventA',
- {service}_event_b: 'eventB',
- {service}_webhook: undefined, // Generic - no filter
- }
-
- const eventType = eventTypeMap[triggerId]
- const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
-
- const requestBody: Record = {
- url: notificationUrl,
- }
-
- if (eventType) {
- requestBody.eventType = eventType
- }
+export const {service}Handler: WebhookProviderHandler = {
+ verifyAuth: createHmacVerifier({
+ configKey: 'webhookSecret',
+ headerName: 'X-{Service}-Signature',
+ validateFn: validate{Service}Signature,
+ providerLabel: '{Service}',
+ }),
- if (projectId) {
- requestBody.projectId = projectId
+ async matchEvent({ body, requestId, providerConfig }: EventMatchContext) {
+ const triggerId = providerConfig.triggerId as string | undefined
+ if (triggerId && triggerId !== '{service}_webhook') {
+ const { is{Service}EventMatch } = await import('@/triggers/{service}/utils')
+ if (!is{Service}EventMatch(triggerId, body as Record)) return false
}
+ return true
+ },
- const response = await fetch('https://api.{service}.com/webhooks', {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${apiKey}`,
- 'Content-Type': 'application/json',
+ async formatInput({ body }: FormatInputContext): Promise {
+ const b = body as Record
+ return {
+ input: {
+ eventType: b.type,
+ resourceId: (b.data as Record)?.id || '',
+ resource: b.data,
},
- body: JSON.stringify(requestBody),
- })
-
- const responseBody = await response.json()
-
- if (!response.ok) {
- const errorMessage = responseBody.message || 'Unknown API error'
- let userFriendlyMessage = 'Failed to create webhook in {Service}'
-
- if (response.status === 401) {
- userFriendlyMessage = 'Invalid API Key. Please verify and try again.'
- } else if (errorMessage) {
- userFriendlyMessage = `{Service} error: ${errorMessage}`
- }
-
- throw new Error(userFriendlyMessage)
}
+ },
- return { id: responseBody.id }
- } catch (error: any) {
- logger.error(`Exception during {Service} webhook creation`, { error: error.message })
- throw error
- }
+ extractIdempotencyId(body: unknown) {
+ const obj = body as Record
+ return obj.id && obj.type ? `${obj.type}:${obj.id}` : null
+ },
}
```
-#### 4. Add Webhook Deletion to Provider Subscriptions
-
-In `apps/sim/lib/webhooks/provider-subscriptions.ts`:
+### Register the Handler
-1. Add a logger:
-```typescript
-const {service}Logger = createLogger('{Service}Webhook')
-```
-
-2. Add the delete function:
-```typescript
-export async function delete{Service}Webhook(webhook: any, requestId: string): Promise {
- try {
- const config = getProviderConfig(webhook)
- const apiKey = config.apiKey as string | undefined
- const externalId = config.externalId as string | undefined
-
- if (!apiKey || !externalId) {
- {service}Logger.warn(`[${requestId}] Missing apiKey or externalId, skipping cleanup`)
- return
- }
-
- const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
- method: 'DELETE',
- headers: {
- Authorization: `Bearer ${apiKey}`,
- },
- })
-
- if (!response.ok && response.status !== 404) {
- {service}Logger.warn(`[${requestId}] Failed to delete webhook (non-fatal): ${response.status}`)
- } else {
- {service}Logger.info(`[${requestId}] Successfully deleted webhook ${externalId}`)
- }
- } catch (error) {
- {service}Logger.warn(`[${requestId}] Error deleting webhook (non-fatal)`, error)
- }
-}
-```
+In `apps/sim/lib/webhooks/providers/registry.ts`:
-3. Add to `cleanupExternalWebhook`:
```typescript
-export async function cleanupExternalWebhook(...): Promise {
- // ... existing providers ...
- } else if (webhook.provider === '{service}') {
- await delete{Service}Webhook(webhook, requestId)
- }
-}
-```
-
-### Key Points for Automatic Registration
-
-- **API Key visibility**: Always use `password: true` for API key fields
-- **Error handling**: Roll back the database webhook if external creation fails
-- **External ID storage**: Save the external webhook ID in `providerConfig.externalId`
-- **Graceful cleanup**: Don't fail webhook deletion if cleanup fails (use non-fatal logging)
-- **User-friendly errors**: Map HTTP status codes to helpful error messages
+import { {service}Handler } from '@/lib/webhooks/providers/{service}'
-## The buildTriggerSubBlocks Helper
-
-This is the generic helper from `@/triggers` that creates consistent trigger subBlocks.
-
-### Function Signature
-
-```typescript
-interface BuildTriggerSubBlocksOptions {
- triggerId: string // e.g., 'service_event_a'
- triggerOptions: Array<{ label: string; id: string }> // Dropdown options
- includeDropdown?: boolean // true only for primary trigger
- setupInstructions: string // HTML instructions
- extraFields?: SubBlockConfig[] // Service-specific fields
- webhookPlaceholder?: string // Custom placeholder text
+const PROVIDER_HANDLERS: Record = {
+ // ... existing (alphabetical) ...
+ {service}: {service}Handler,
}
-
-function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): SubBlockConfig[]
```
-### What It Creates
+## Output Alignment (Critical)
-The helper creates this structure:
-1. **Dropdown** (only if `includeDropdown: true`) - Trigger type selector
-2. **Webhook URL** - Read-only field with copy button
-3. **Extra Fields** - Your service-specific fields (filters, options, etc.)
-4. **Save Button** - Activates the trigger
-5. **Instructions** - Setup guide for users
+There are two sources of truth that **MUST be aligned**:
-All fields automatically have:
-- `mode: 'trigger'` - Only shown in trigger mode
-- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
+1. **Trigger `outputs`** — schema defining what fields SHOULD be available (UI tag dropdown)
+2. **`formatInput` on the handler** — implementation that transforms raw payload into actual data
-## Trigger Outputs & Webhook Input Formatting
+If they differ: the tag dropdown shows fields that don't exist, or actual data has fields users can't discover.
-### Important: Two Sources of Truth
+**Rules for `formatInput`:**
+- Return `{ input: { ... } }` where inner keys match trigger `outputs` exactly
+- Return `{ input: ..., skip: { message: '...' } }` to skip execution
+- No wrapper objects or duplication
+- Use `null` for missing optional data
-There are two related but separate concerns:
+## Automatic Webhook Registration
-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`.
+If the service API supports programmatic webhook creation, implement `createSubscription` and `deleteSubscription` on the handler. The orchestration layer calls these automatically — **no code touches `route.ts`, `provider-subscriptions.ts`, or `deploy.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
+```typescript
+import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
+import type { DeleteSubscriptionContext, SubscriptionContext, SubscriptionResult } from '@/lib/webhooks/providers/types'
-- **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.
+export const {service}Handler: WebhookProviderHandler = {
+ async createSubscription(ctx: SubscriptionContext): Promise {
+ const config = getProviderConfig(ctx.webhook)
+ const apiKey = config.apiKey as string
+ if (!apiKey) throw new Error('{Service} API Key is required.')
-### Adding a Handler
+ const res = await fetch('https://api.{service}.com/webhooks', {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url: getNotificationUrl(ctx.webhook) }),
+ })
-In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block:
+ if (!res.ok) throw new Error(`{Service} error: ${res.status}`)
+ const { id } = (await res.json()) as { id: string }
+ return { providerConfigUpdates: { externalId: id } }
+ },
-```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,
- }
+ async deleteSubscription(ctx: DeleteSubscriptionContext): Promise {
+ const config = getProviderConfig(ctx.webhook)
+ const { apiKey, externalId } = config as { apiKey?: string; externalId?: string }
+ if (!apiKey || !externalId) return
+ await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
+ method: 'DELETE',
+ headers: { Authorization: `Bearer ${apiKey}` },
+ }).catch(() => {})
+ },
}
```
-**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
+**Key points:**
+- Throw from `createSubscription` — orchestration rolls back the DB webhook
+- Never throw from `deleteSubscription` — log non-fatally
+- Return `{ providerConfigUpdates: { externalId } }` — orchestration merges into `providerConfig`
+- Add `apiKey` field to `build{Service}ExtraFields` with `password: true`
-### Verify Alignment
-
-Run the alignment checker:
-```bash
-bunx scripts/check-trigger-alignment.ts {service}
-```
-
-## Trigger Outputs
+## Trigger Outputs Schema
Trigger outputs use the same schema as block outputs (NOT tool outputs).
-**Supported:**
-- `type` and `description` for simple fields
-- Nested object structure for complex data
-
-**NOT Supported:**
-- `optional: true` (tool outputs only)
-- `items` property (tool outputs only)
+**Supported:** `type` + `description` for leaf fields, nested objects for complex data.
+**NOT supported:** `optional: true`, `items` (those are tool-output-only features).
```typescript
export function buildOutputs(): Record {
return {
- // Simple fields
eventType: { type: 'string', description: 'Event type' },
timestamp: { type: 'string', description: 'When it occurred' },
-
- // Complex data - use type: 'json'
payload: { type: 'json', description: 'Full event payload' },
-
- // Nested structure
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
@@ -630,79 +327,32 @@ export function buildOutputs(): Record {
}
```
-## Generic Webhook Trigger Pattern
-
-For services with many event types, create a generic webhook that accepts all events:
-
-```typescript
-export const {service}WebhookTrigger: TriggerConfig = {
- id: '{service}_webhook',
- name: '{Service} Webhook (All Events)',
- // ...
-
- subBlocks: buildTriggerSubBlocks({
- triggerId: '{service}_webhook',
- triggerOptions: {service}TriggerOptions,
- setupInstructions: {service}SetupInstructions('All Events'),
- extraFields: [
- // Event type filter (optional)
- {
- id: 'eventTypes',
- title: 'Event Types',
- type: 'dropdown',
- multiSelect: true,
- options: [
- { label: 'Event A', id: 'event_a' },
- { label: 'Event B', id: 'event_b' },
- ],
- placeholder: 'Leave empty for all events',
- mode: 'trigger',
- condition: { field: 'selectedTriggerId', value: '{service}_webhook' },
- },
- // Plus any other service-specific fields
- ...build{Service}ExtraFields('{service}_webhook'),
- ],
- }),
-}
-```
-
-## Checklist Before Finishing
-
-### Utils
-- [ ] Created `{service}TriggerOptions` array with all trigger IDs
-- [ ] Created `{service}SetupInstructions` function with clear steps
-- [ ] Created `build{Service}ExtraFields` for service-specific fields
-- [ ] Created output builders for each trigger type
+## Checklist
-### Triggers
-- [ ] Primary trigger has `includeDropdown: true`
-- [ ] Secondary triggers do NOT have `includeDropdown`
+### Trigger Definition
+- [ ] Created `utils.ts` with options, instructions, extra fields, and output builders
+- [ ] Primary trigger has `includeDropdown: true`; secondary triggers do NOT
- [ ] All triggers use `buildTriggerSubBlocks` helper
-- [ ] All triggers have proper outputs defined
- [ ] Created `index.ts` barrel export
### Registration
-- [ ] All triggers imported in `triggers/registry.ts`
-- [ ] All triggers added to `TRIGGER_REGISTRY`
-- [ ] Block has `triggers.enabled: true`
-- [ ] Block has all trigger IDs in `triggers.available`
+- [ ] All triggers in `triggers/registry.ts` → `TRIGGER_REGISTRY`
+- [ ] Block has `triggers.enabled: true` and lists all trigger IDs in `triggers.available`
- [ ] Block spreads all trigger subBlocks: `...getTrigger('id').subBlocks`
-### Automatic Webhook Registration (if supported)
-- [ ] Added API key field to `build{Service}ExtraFields` with `password: true`
-- [ ] Updated setup instructions for automatic webhook creation
-- [ ] Added provider-specific logic to `apps/sim/app/api/webhooks/route.ts`
-- [ ] Added `create{Service}WebhookSubscription` helper function
-- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
-- [ ] Added provider to `cleanupExternalWebhook` function
+### Provider Handler (if needed)
+- [ ] Handler file at `apps/sim/lib/webhooks/providers/{service}.ts`
+- [ ] Registered in `providers/registry.ts` (alphabetical)
+- [ ] Signature validator is a private function inside the handler file
+- [ ] `formatInput` output keys match trigger `outputs` exactly
+- [ ] Event matching uses dynamic `await import()` for trigger utils
-### 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
+### Auto Registration (if supported)
+- [ ] `createSubscription` and `deleteSubscription` on the handler
+- [ ] NO changes to `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
+- [ ] API key field uses `password: true`
### Testing
-- [ ] Run `bun run type-check` to verify no TypeScript errors
-- [ ] Restart dev server to pick up new triggers
-- [ ] Test trigger UI shows correctly in the block
-- [ ] Test automatic webhook creation works (if applicable)
+- [ ] `bun run type-check` passes
+- [ ] Manually verify `formatInput` output keys match trigger `outputs` keys
+- [ ] Trigger UI shows correctly in the block
diff --git a/.agents/skills/validate-trigger/SKILL.md b/.agents/skills/validate-trigger/SKILL.md
new file mode 100644
index 00000000000..ff1eb775b44
--- /dev/null
+++ b/.agents/skills/validate-trigger/SKILL.md
@@ -0,0 +1,212 @@
+---
+name: validate-trigger
+description: Audit an existing Sim webhook trigger against the service's webhook API docs and repository conventions, then report and fix issues across trigger definitions, provider handler, output alignment, registration, and security. Use when validating or repairing a trigger under `apps/sim/triggers/{service}/` or `apps/sim/lib/webhooks/providers/{service}.ts`.
+---
+
+# Validate Trigger
+
+You are an expert auditor for Sim webhook triggers. Your job is to validate that an existing trigger implementation is correct, complete, secure, and aligned across all layers.
+
+## Your Task
+
+1. Read the service's webhook/API documentation (via WebFetch)
+2. Read every trigger file, provider handler, and registry entry
+3. Cross-reference against the API docs and Sim conventions
+4. Report all issues grouped by severity (critical, warning, suggestion)
+5. Fix all issues after reporting them
+
+## Step 1: Gather All Files
+
+Read **every** file for the trigger — do not skip any:
+
+```
+apps/sim/triggers/{service}/ # All trigger files, utils.ts, index.ts
+apps/sim/lib/webhooks/providers/{service}.ts # Provider handler (if exists)
+apps/sim/lib/webhooks/providers/registry.ts # Handler registry
+apps/sim/triggers/registry.ts # Trigger registry
+apps/sim/blocks/blocks/{service}.ts # Block definition (trigger wiring)
+```
+
+Also read for reference:
+```
+apps/sim/lib/webhooks/providers/types.ts # WebhookProviderHandler interface
+apps/sim/lib/webhooks/providers/utils.ts # Shared helpers (createHmacVerifier, etc.)
+apps/sim/lib/webhooks/provider-subscription-utils.ts # Subscription helpers
+apps/sim/lib/webhooks/processor.ts # Central webhook processor
+```
+
+## Step 2: Pull API Documentation
+
+Fetch the service's official webhook documentation. This is the **source of truth** for:
+- Webhook event types and payload shapes
+- Signature/auth verification method (HMAC algorithm, header names, secret format)
+- Challenge/verification handshake requirements
+- Webhook subscription API (create/delete endpoints, if applicable)
+- Retry behavior and delivery guarantees
+
+## Step 3: Validate Trigger Definitions
+
+### utils.ts
+- [ ] `{service}TriggerOptions` lists all trigger IDs accurately
+- [ ] `{service}SetupInstructions` provides clear, correct steps for the service
+- [ ] `build{Service}ExtraFields` includes relevant filter/config fields with correct `condition`
+- [ ] Output builders expose all meaningful fields from the webhook payload
+- [ ] Output builders do NOT use `optional: true` or `items` (tool-output-only features)
+- [ ] Nested output objects correctly model the payload structure
+
+### Trigger Files
+- [ ] Exactly one primary trigger has `includeDropdown: true`
+- [ ] All secondary triggers do NOT have `includeDropdown`
+- [ ] All triggers use `buildTriggerSubBlocks` helper (not hand-rolled subBlocks)
+- [ ] Every trigger's `id` matches the convention `{service}_{event_name}`
+- [ ] Every trigger's `provider` matches the service name used in the handler registry
+- [ ] `index.ts` barrel exports all triggers
+
+### Trigger ↔ Provider Alignment (CRITICAL)
+- [ ] Every trigger ID referenced in `matchEvent` logic exists in `{service}TriggerOptions`
+- [ ] Event matching logic in the provider correctly maps trigger IDs to service event types
+- [ ] Event matching logic in `is{Service}EventMatch` (if exists) correctly identifies events per the API docs
+
+## Step 4: Validate Provider Handler
+
+### Auth Verification
+- [ ] `verifyAuth` correctly validates webhook signatures per the service's documentation
+- [ ] HMAC algorithm matches (SHA-1, SHA-256, SHA-512)
+- [ ] Signature header name matches the API docs exactly
+- [ ] Signature format is handled (raw hex, `sha256=` prefix, base64, etc.)
+- [ ] Uses `safeCompare` for timing-safe comparison (no `===`)
+- [ ] If `webhookSecret` is required, handler rejects when it's missing (fail-closed)
+- [ ] Signature is computed over raw body (not parsed JSON)
+
+### Event Matching
+- [ ] `matchEvent` returns `boolean` (not `NextResponse` or other values)
+- [ ] Challenge/verification events are excluded from matching (e.g., `endpoint.url_validation`)
+- [ ] When `triggerId` is a generic webhook ID, all events pass through
+- [ ] When `triggerId` is specific, only matching events pass
+- [ ] Event matching logic uses dynamic `await import()` for trigger utils (avoids circular deps)
+
+### formatInput (CRITICAL)
+- [ ] Every key in the `formatInput` return matches a key in the trigger `outputs` schema
+- [ ] Every key in the trigger `outputs` schema is populated by `formatInput`
+- [ ] No extra undeclared keys that users can't discover in the UI
+- [ ] No wrapper objects (`webhook: { ... }`, `{service}: { ... }`)
+- [ ] Nested output paths exist at the correct depth (e.g., `resource.id` actually has `resource: { id: ... }`)
+- [ ] `null` is used for missing optional fields (not empty strings or empty objects)
+- [ ] Returns `{ input: { ... } }` — not a bare object
+
+### Idempotency
+- [ ] `extractIdempotencyId` returns a stable, unique key per delivery
+- [ ] Uses provider-specific delivery IDs when available (e.g., `X-Request-Id`, `Linear-Delivery`, `svix-id`)
+- [ ] Falls back to content-based ID (e.g., `${type}:${id}`) when no delivery header exists
+- [ ] Does NOT include timestamps in the idempotency key (would break dedup on retries)
+
+### Challenge Handling (if applicable)
+- [ ] `handleChallenge` correctly implements the service's URL verification handshake
+- [ ] Returns the expected response format per the API docs
+- [ ] Env-backed secrets are resolved via `resolveEnvVarsInObject` if needed
+
+## Step 5: Validate Automatic Subscription Lifecycle
+
+If the service supports programmatic webhook creation:
+
+### createSubscription
+- [ ] Calls the correct API endpoint to create a webhook
+- [ ] Sends the correct event types/filters
+- [ ] Passes the notification URL from `getNotificationUrl(ctx.webhook)`
+- [ ] Returns `{ providerConfigUpdates: { externalId } }` with the external webhook ID
+- [ ] Throws on failure (orchestration handles rollback)
+- [ ] Provides user-friendly error messages (401 → "Invalid API Key", etc.)
+
+### deleteSubscription
+- [ ] Calls the correct API endpoint to delete the webhook
+- [ ] Handles 404 gracefully (webhook already deleted)
+- [ ] Never throws — catches errors and logs non-fatally
+- [ ] Skips gracefully when `apiKey` or `externalId` is missing
+
+### Orchestration Isolation
+- [ ] NO provider-specific logic in `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
+- [ ] All subscription logic lives on the handler (`createSubscription`/`deleteSubscription`)
+
+## Step 6: Validate Registration and Block Wiring
+
+### Trigger Registry (`triggers/registry.ts`)
+- [ ] All triggers are imported and registered
+- [ ] Registry keys match trigger IDs exactly
+- [ ] No orphaned entries (triggers that don't exist)
+
+### Provider Handler Registry (`providers/registry.ts`)
+- [ ] Handler is imported and registered (if handler exists)
+- [ ] Registry key matches the `provider` field on the trigger configs
+- [ ] Entries are in alphabetical order
+
+### Block Wiring (`blocks/blocks/{service}.ts`)
+- [ ] Block has `triggers.enabled: true`
+- [ ] `triggers.available` lists all trigger IDs
+- [ ] All trigger subBlocks are spread into `subBlocks`: `...getTrigger('id').subBlocks`
+- [ ] No trigger IDs in `triggers.available` that aren't in the registry
+- [ ] No trigger subBlocks spread that aren't in `triggers.available`
+
+## Step 7: Validate Security
+
+- [ ] Webhook secrets are never logged (not even at debug level)
+- [ ] Auth verification runs before any event processing
+- [ ] No secret comparison uses `===` (must use `safeCompare` or `crypto.timingSafeEqual`)
+- [ ] Timestamp/replay protection is reasonable (not too tight for retries, not too loose for security)
+- [ ] Raw body is used for signature verification (not re-serialized JSON)
+
+## Step 8: Report and Fix
+
+### Report Format
+
+Group findings by severity:
+
+**Critical** (runtime errors, security issues, or data loss):
+- Wrong HMAC algorithm or header name
+- `formatInput` keys don't match trigger `outputs`
+- Missing `verifyAuth` when the service sends signed webhooks
+- `matchEvent` returns non-boolean values
+- Provider-specific logic leaking into shared orchestration files
+- Trigger IDs mismatch between trigger files, registry, and block
+- `createSubscription` calling wrong API endpoint
+- Auth comparison using `===` instead of `safeCompare`
+
+**Warning** (convention violations or usability issues):
+- Missing `extractIdempotencyId` when the service provides delivery IDs
+- Timestamps in idempotency keys (breaks dedup on retries)
+- Missing challenge handling when the service requires URL verification
+- Output schema missing fields that `formatInput` returns (undiscoverable data)
+- Overly tight timestamp skew window that rejects legitimate retries
+- `matchEvent` not filtering challenge/verification events
+- Setup instructions missing important steps
+
+**Suggestion** (minor improvements):
+- More specific output field descriptions
+- Additional output fields that could be exposed
+- Better error messages in `createSubscription`
+- Logging improvements
+
+### Fix All Issues
+
+After reporting, fix every **critical** and **warning** issue. Apply **suggestions** where they don't add unnecessary complexity.
+
+### Validation Output
+
+After fixing, confirm:
+1. `bun run type-check` passes
+2. Re-read all modified files to verify fixes are correct
+3. Provider handler tests pass (if they exist): `bun test {service}`
+
+## Checklist Summary
+
+- [ ] Read all trigger files, provider handler, types, registries, and block
+- [ ] Pulled and read official webhook/API documentation
+- [ ] Validated trigger definitions: options, instructions, extra fields, outputs
+- [ ] Validated primary/secondary trigger distinction (`includeDropdown`)
+- [ ] Validated provider handler: auth, matchEvent, formatInput, idempotency
+- [ ] Validated output alignment: every `outputs` key ↔ every `formatInput` key
+- [ ] Validated subscription lifecycle: createSubscription, deleteSubscription, no shared-file edits
+- [ ] Validated registration: trigger registry, handler registry, block wiring
+- [ ] Validated security: safe comparison, no secret logging, replay protection
+- [ ] Reported all issues grouped by severity
+- [ ] Fixed all critical and warning issues
+- [ ] `bun run type-check` passes after fixes
diff --git a/.claude/commands/add-trigger.md b/.claude/commands/add-trigger.md
index d252bf61666..9cbeca68a3e 100644
--- a/.claude/commands/add-trigger.md
+++ b/.claude/commands/add-trigger.md
@@ -3,63 +3,57 @@ description: Create webhook triggers for a Sim integration using the generic tri
argument-hint:
---
-# Add Trigger Skill
+# Add Trigger
You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
## Your Task
-When the user asks you to create triggers for a service:
1. Research what webhook events the service supports
2. Create the trigger files using the generic builder
-3. Register triggers and connect them to the block
+3. Create a provider handler if custom auth, formatting, or subscriptions are needed
+4. Register triggers and connect them to the block
## Directory Structure
```
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
-├── utils.ts # Service-specific helpers (trigger options, setup instructions, extra fields)
+├── utils.ts # Service-specific helpers (options, instructions, extra fields, outputs)
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.ts # Secondary trigger (no dropdown)
-├── {event_c}.ts # Secondary trigger (no dropdown)
└── webhook.ts # Generic webhook trigger (optional, for "all events")
+
+apps/sim/lib/webhooks/
+├── provider-subscription-utils.ts # Shared subscription helpers (getProviderConfig, getNotificationUrl)
+├── providers/
+│ ├── {service}.ts # Provider handler (auth, formatInput, matchEvent, subscriptions)
+│ ├── types.ts # WebhookProviderHandler interface
+│ ├── utils.ts # Shared helpers (createHmacVerifier, verifyTokenAuth, skipByEventTypes)
+│ └── registry.ts # Handler map + default handler
```
-## Step 1: Create utils.ts
+## Step 1: Create `utils.ts`
-This file contains service-specific helpers used by all triggers.
+This file contains all service-specific helpers used by triggers.
```typescript
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
-/**
- * Dropdown options for the trigger type selector.
- * These appear in the primary trigger's dropdown.
- */
export const {service}TriggerOptions = [
{ label: 'Event A', id: '{service}_event_a' },
{ label: 'Event B', id: '{service}_event_b' },
- { label: 'Event C', id: '{service}_event_c' },
- { label: 'Generic Webhook (All Events)', id: '{service}_webhook' },
]
-/**
- * Generates HTML setup instructions for the trigger.
- * Displayed to users to help them configure webhooks in the external service.
- */
export function {service}SetupInstructions(eventType: string): string {
const instructions = [
'Copy the Webhook URL above',
'Go to {Service} Settings > Webhooks',
- 'Click Add Webhook',
- 'Paste the webhook URL',
`Select the ${eventType} event type`,
- 'Save the webhook configuration',
+ 'Paste the webhook URL and save',
'Click "Save" above to activate your trigger',
]
-
return instructions
.map((instruction, index) =>
`
${index + 1}. ${instruction}
`
@@ -67,10 +61,6 @@ export function {service}SetupInstructions(eventType: string): string {
.join('')
}
-/**
- * Service-specific extra fields to add to triggers.
- * These are inserted between webhookUrl and triggerSave.
- */
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
@@ -78,53 +68,34 @@ export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
title: 'Project ID (Optional)',
type: 'short-input',
placeholder: 'Leave empty for all projects',
- description: 'Optionally filter to a specific project',
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
-/**
- * Build outputs for this trigger type.
- * Outputs define what data is available to downstream blocks.
- */
export function build{Service}Outputs(): Record {
return {
- eventType: { type: 'string', description: 'The type of event that triggered this workflow' },
+ eventType: { type: 'string', description: 'The type of event' },
resourceId: { type: 'string', description: 'ID of the affected resource' },
- timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
- // Nested outputs for complex data
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
- status: { type: 'string', description: 'Current status' },
},
- webhook: { type: 'json', description: 'Full webhook payload' },
}
}
```
-## Step 2: Create the Primary Trigger
+## Step 2: Create Trigger Files
-The **primary trigger** is the first one listed. It MUST include `includeDropdown: true` so users can switch between trigger types.
+**Primary trigger** — MUST include `includeDropdown: true`:
```typescript
import { {Service}Icon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
-import {
- build{Service}ExtraFields,
- build{Service}Outputs,
- {service}SetupInstructions,
- {service}TriggerOptions,
-} from '@/triggers/{service}/utils'
+import { build{Service}ExtraFields, build{Service}Outputs, {service}SetupInstructions, {service}TriggerOptions } from '@/triggers/{service}/utils'
import type { TriggerConfig } from '@/triggers/types'
-/**
- * {Service} Event A Trigger
- *
- * This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
- */
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
name: '{Service} Event A',
@@ -132,496 +103,222 @@ export const {service}EventATrigger: TriggerConfig = {
description: 'Trigger workflow when Event A occurs',
version: '1.0.0',
icon: {Service}Icon,
-
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_a',
triggerOptions: {service}TriggerOptions,
- includeDropdown: true, // PRIMARY TRIGGER - includes dropdown
+ includeDropdown: true,
setupInstructions: {service}SetupInstructions('Event A'),
extraFields: build{Service}ExtraFields('{service}_event_a'),
}),
-
outputs: build{Service}Outputs(),
-
- webhook: {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- },
+ webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } },
}
```
-## Step 3: Create Secondary Triggers
-
-Secondary triggers do NOT include the dropdown (it's already in the primary trigger).
+**Secondary triggers** — NO `includeDropdown` (it's already in the primary):
```typescript
-import { {Service}Icon } from '@/components/icons'
-import { buildTriggerSubBlocks } from '@/triggers'
-import {
- build{Service}ExtraFields,
- build{Service}Outputs,
- {service}SetupInstructions,
- {service}TriggerOptions,
-} from '@/triggers/{service}/utils'
-import type { TriggerConfig } from '@/triggers/types'
-
-/**
- * {Service} Event B Trigger
- */
export const {service}EventBTrigger: TriggerConfig = {
- id: '{service}_event_b',
- name: '{Service} Event B',
- provider: '{service}',
- description: 'Trigger workflow when Event B occurs',
- version: '1.0.0',
- icon: {Service}Icon,
-
- subBlocks: buildTriggerSubBlocks({
- triggerId: '{service}_event_b',
- triggerOptions: {service}TriggerOptions,
- // NO includeDropdown - secondary trigger
- setupInstructions: {service}SetupInstructions('Event B'),
- extraFields: build{Service}ExtraFields('{service}_event_b'),
- }),
-
- outputs: build{Service}Outputs(),
-
- webhook: {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- },
+ // Same as above but: id: '{service}_event_b', no includeDropdown
}
```
-## Step 4: Create index.ts Barrel Export
+## Step 3: Register and Wire
+
+### `apps/sim/triggers/{service}/index.ts`
```typescript
export { {service}EventATrigger } from './event_a'
export { {service}EventBTrigger } from './event_b'
-export { {service}EventCTrigger } from './event_c'
-export { {service}WebhookTrigger } from './webhook'
```
-## Step 5: Register Triggers
-
-### Trigger Registry (`apps/sim/triggers/registry.ts`)
+### `apps/sim/triggers/registry.ts`
```typescript
-// Add import
-import {
- {service}EventATrigger,
- {service}EventBTrigger,
- {service}EventCTrigger,
- {service}WebhookTrigger,
-} from '@/triggers/{service}'
-
-// Add to TRIGGER_REGISTRY
+import { {service}EventATrigger, {service}EventBTrigger } from '@/triggers/{service}'
+
export const TRIGGER_REGISTRY: TriggerRegistry = {
- // ... existing triggers ...
+ // ... existing ...
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
- {service}_event_c: {service}EventCTrigger,
- {service}_webhook: {service}WebhookTrigger,
}
```
-## Step 6: Connect Triggers to Block
-
-In the block file (`apps/sim/blocks/blocks/{service}.ts`):
+### Block file (`apps/sim/blocks/blocks/{service}.ts`)
```typescript
-import { {Service}Icon } from '@/components/icons'
import { getTrigger } from '@/triggers'
-import type { BlockConfig } from '@/blocks/types'
export const {Service}Block: BlockConfig = {
- type: '{service}',
- name: '{Service}',
- // ... other config ...
-
- // Enable triggers and list available trigger IDs
+ // ...
triggers: {
enabled: true,
- available: [
- '{service}_event_a',
- '{service}_event_b',
- '{service}_event_c',
- '{service}_webhook',
- ],
+ available: ['{service}_event_a', '{service}_event_b'],
},
-
subBlocks: [
- // Regular tool subBlocks first
- { id: 'operation', /* ... */ },
- { id: 'credential', /* ... */ },
- // ... other tool fields ...
-
- // Then spread ALL trigger subBlocks
+ // Regular tool subBlocks first...
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
- ...getTrigger('{service}_event_c').subBlocks,
- ...getTrigger('{service}_webhook').subBlocks,
],
-
- // ... tools config ...
}
```
-## Automatic Webhook Registration (Preferred)
-
-If the service's API supports programmatic webhook creation, implement automatic webhook registration instead of requiring users to manually configure webhooks. This provides a much better user experience.
+## Provider Handler
-### When to Use Automatic Registration
+All provider-specific webhook logic lives in a single handler file: `apps/sim/lib/webhooks/providers/{service}.ts`.
-Check the service's API documentation for endpoints like:
-- `POST /webhooks` or `POST /hooks` - Create webhook
-- `DELETE /webhooks/{id}` - Delete webhook
+### When to Create a Handler
-Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, etc.
+| Behavior | Method | Examples |
+|---|---|---|
+| HMAC signature auth | `verifyAuth` via `createHmacVerifier` | Ashby, Jira, Linear, Typeform |
+| Custom token auth | `verifyAuth` via `verifyTokenAuth` | Generic, Google Forms |
+| Event filtering | `matchEvent` | GitHub, Jira, Attio, HubSpot |
+| Idempotency dedup | `extractIdempotencyId` | Slack, Stripe, Linear, Jira |
+| Custom input formatting | `formatInput` | Slack, Teams, Attio, Ashby |
+| Auto webhook creation | `createSubscription` | Ashby, Grain, Calendly, Airtable |
+| Auto webhook deletion | `deleteSubscription` | Ashby, Grain, Calendly, Airtable |
+| Challenge/verification | `handleChallenge` | Slack, WhatsApp, Teams |
+| Custom success response | `formatSuccessResponse` | Slack, Twilio Voice, Teams |
-### Implementation Steps
+If none apply, you don't need a handler. The default handler provides bearer token auth.
-#### 1. Add API Key to Extra Fields
-
-Update your `build{Service}ExtraFields` function to include an API key field:
+### Example Handler
```typescript
-export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
- return [
- {
- id: 'apiKey',
- title: 'API Key',
- type: 'short-input',
- placeholder: 'Enter your {Service} API key',
- description: 'Required to create the webhook in {Service}.',
- password: true,
- required: true,
- mode: 'trigger',
- condition: { field: 'selectedTriggerId', value: triggerId },
- },
- // Other optional fields (e.g., campaign filter, project filter)
- {
- id: 'projectId',
- title: 'Project ID (Optional)',
- type: 'short-input',
- placeholder: 'Leave empty for all projects',
- mode: 'trigger',
- condition: { field: 'selectedTriggerId', value: triggerId },
- },
- ]
+import crypto from 'crypto'
+import { createLogger } from '@sim/logger'
+import { safeCompare } from '@/lib/core/security/encryption'
+import type { EventMatchContext, FormatInputContext, FormatInputResult, WebhookProviderHandler } from '@/lib/webhooks/providers/types'
+import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
+
+const logger = createLogger('WebhookProvider:{Service}')
+
+function validate{Service}Signature(secret: string, signature: string, body: string): boolean {
+ if (!secret || !signature || !body) return false
+ const computed = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
+ return safeCompare(computed, signature)
}
-```
-
-#### 2. Update Setup Instructions for Automatic Creation
-
-Change instructions to indicate automatic webhook creation:
-
-```typescript
-export function {service}SetupInstructions(eventType: string): string {
- const instructions = [
- 'Enter your {Service} API Key above.',
- 'You can find your API key in {Service} at Settings > API.',
- `Click "Save Configuration" to automatically create the webhook in {Service} for ${eventType} events.`,
- 'The webhook will be automatically deleted when you remove this trigger.',
- ]
- return instructions
- .map((instruction, index) =>
- `
${index + 1}. ${instruction}
`
- )
- .join('')
-}
-```
-
-#### 3. Add Webhook Creation to API Route
-
-In `apps/sim/app/api/webhooks/route.ts`, add provider-specific logic after the database save:
-
-```typescript
-// --- {Service} specific logic ---
-if (savedWebhook && provider === '{service}') {
- logger.info(`[${requestId}] {Service} provider detected. Creating webhook subscription.`)
- try {
- const result = await create{Service}WebhookSubscription(
- {
- id: savedWebhook.id,
- path: savedWebhook.path,
- providerConfig: savedWebhook.providerConfig,
- },
- requestId
- )
-
- if (result) {
- // Update the webhook record with the external webhook ID
- const updatedConfig = {
- ...(savedWebhook.providerConfig as Record),
- externalId: result.id,
- }
- await db
- .update(webhook)
- .set({
- providerConfig: updatedConfig,
- updatedAt: new Date(),
- })
- .where(eq(webhook.id, savedWebhook.id))
-
- savedWebhook.providerConfig = updatedConfig
- logger.info(`[${requestId}] Successfully created {Service} webhook`, {
- externalHookId: result.id,
- webhookId: savedWebhook.id,
- })
- }
- } catch (err) {
- logger.error(
- `[${requestId}] Error creating {Service} webhook subscription, rolling back webhook`,
- err
- )
- await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
- return NextResponse.json(
- {
- error: 'Failed to create webhook in {Service}',
- details: err instanceof Error ? err.message : 'Unknown error',
- },
- { status: 500 }
- )
- }
-}
-// --- End {Service} specific logic ---
-```
-
-Then add the helper function at the end of the file:
-
-```typescript
-async function create{Service}WebhookSubscription(
- webhookData: any,
- requestId: string
-): Promise<{ id: string } | undefined> {
- try {
- const { path, providerConfig } = webhookData
- const { apiKey, triggerId, projectId } = providerConfig || {}
-
- if (!apiKey) {
- throw new Error('{Service} API Key is required.')
- }
-
- // Map trigger IDs to service event types
- const eventTypeMap: Record = {
- {service}_event_a: 'eventA',
- {service}_event_b: 'eventB',
- {service}_webhook: undefined, // Generic - no filter
- }
-
- const eventType = eventTypeMap[triggerId]
- const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
-
- const requestBody: Record = {
- url: notificationUrl,
- }
-
- if (eventType) {
- requestBody.eventType = eventType
- }
+export const {service}Handler: WebhookProviderHandler = {
+ verifyAuth: createHmacVerifier({
+ configKey: 'webhookSecret',
+ headerName: 'X-{Service}-Signature',
+ validateFn: validate{Service}Signature,
+ providerLabel: '{Service}',
+ }),
- if (projectId) {
- requestBody.projectId = projectId
+ async matchEvent({ body, requestId, providerConfig }: EventMatchContext) {
+ const triggerId = providerConfig.triggerId as string | undefined
+ if (triggerId && triggerId !== '{service}_webhook') {
+ const { is{Service}EventMatch } = await import('@/triggers/{service}/utils')
+ if (!is{Service}EventMatch(triggerId, body as Record)) return false
}
+ return true
+ },
- const response = await fetch('https://api.{service}.com/webhooks', {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${apiKey}`,
- 'Content-Type': 'application/json',
+ async formatInput({ body }: FormatInputContext): Promise {
+ const b = body as Record
+ return {
+ input: {
+ eventType: b.type,
+ resourceId: (b.data as Record)?.id || '',
+ resource: b.data,
},
- body: JSON.stringify(requestBody),
- })
-
- const responseBody = await response.json()
-
- if (!response.ok) {
- const errorMessage = responseBody.message || 'Unknown API error'
- let userFriendlyMessage = 'Failed to create webhook in {Service}'
-
- if (response.status === 401) {
- userFriendlyMessage = 'Invalid API Key. Please verify and try again.'
- } else if (errorMessage) {
- userFriendlyMessage = `{Service} error: ${errorMessage}`
- }
-
- throw new Error(userFriendlyMessage)
}
+ },
- return { id: responseBody.id }
- } catch (error: any) {
- logger.error(`Exception during {Service} webhook creation`, { error: error.message })
- throw error
- }
+ extractIdempotencyId(body: unknown) {
+ const obj = body as Record
+ return obj.id && obj.type ? `${obj.type}:${obj.id}` : null
+ },
}
```
-#### 4. Add Webhook Deletion to Provider Subscriptions
-
-In `apps/sim/lib/webhooks/provider-subscriptions.ts`:
+### Register the Handler
-1. Add a logger:
-```typescript
-const {service}Logger = createLogger('{Service}Webhook')
-```
-
-2. Add the delete function:
-```typescript
-export async function delete{Service}Webhook(webhook: any, requestId: string): Promise {
- try {
- const config = getProviderConfig(webhook)
- const apiKey = config.apiKey as string | undefined
- const externalId = config.externalId as string | undefined
-
- if (!apiKey || !externalId) {
- {service}Logger.warn(`[${requestId}] Missing apiKey or externalId, skipping cleanup`)
- return
- }
-
- const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
- method: 'DELETE',
- headers: {
- Authorization: `Bearer ${apiKey}`,
- },
- })
-
- if (!response.ok && response.status !== 404) {
- {service}Logger.warn(`[${requestId}] Failed to delete webhook (non-fatal): ${response.status}`)
- } else {
- {service}Logger.info(`[${requestId}] Successfully deleted webhook ${externalId}`)
- }
- } catch (error) {
- {service}Logger.warn(`[${requestId}] Error deleting webhook (non-fatal)`, error)
- }
-}
-```
+In `apps/sim/lib/webhooks/providers/registry.ts`:
-3. Add to `cleanupExternalWebhook`:
```typescript
-export async function cleanupExternalWebhook(...): Promise {
- // ... existing providers ...
- } else if (webhook.provider === '{service}') {
- await delete{Service}Webhook(webhook, requestId)
- }
-}
-```
-
-### Key Points for Automatic Registration
-
-- **API Key visibility**: Always use `password: true` for API key fields
-- **Error handling**: Roll back the database webhook if external creation fails
-- **External ID storage**: Save the external webhook ID in `providerConfig.externalId`
-- **Graceful cleanup**: Don't fail webhook deletion if cleanup fails (use non-fatal logging)
-- **User-friendly errors**: Map HTTP status codes to helpful error messages
+import { {service}Handler } from '@/lib/webhooks/providers/{service}'
-## The buildTriggerSubBlocks Helper
-
-This is the generic helper from `@/triggers` that creates consistent trigger subBlocks.
-
-### Function Signature
-
-```typescript
-interface BuildTriggerSubBlocksOptions {
- triggerId: string // e.g., 'service_event_a'
- triggerOptions: Array<{ label: string; id: string }> // Dropdown options
- includeDropdown?: boolean // true only for primary trigger
- setupInstructions: string // HTML instructions
- extraFields?: SubBlockConfig[] // Service-specific fields
- webhookPlaceholder?: string // Custom placeholder text
+const PROVIDER_HANDLERS: Record = {
+ // ... existing (alphabetical) ...
+ {service}: {service}Handler,
}
-
-function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): SubBlockConfig[]
```
-### What It Creates
+## Output Alignment (Critical)
-The helper creates this structure:
-1. **Dropdown** (only if `includeDropdown: true`) - Trigger type selector
-2. **Webhook URL** - Read-only field with copy button
-3. **Extra Fields** - Your service-specific fields (filters, options, etc.)
-4. **Save Button** - Activates the trigger
-5. **Instructions** - Setup guide for users
+There are two sources of truth that **MUST be aligned**:
-All fields automatically have:
-- `mode: 'trigger'` - Only shown in trigger mode
-- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
+1. **Trigger `outputs`** — schema defining what fields SHOULD be available (UI tag dropdown)
+2. **`formatInput` on the handler** — implementation that transforms raw payload into actual data
-## Trigger Outputs & Webhook Input Formatting
+If they differ: the tag dropdown shows fields that don't exist, or actual data has fields users can't discover.
-### Important: Two Sources of Truth
+**Rules for `formatInput`:**
+- Return `{ input: { ... } }` where inner keys match trigger `outputs` exactly
+- Return `{ input: ..., skip: { message: '...' } }` to skip execution
+- No wrapper objects or duplication
+- Use `null` for missing optional data
-There are two related but separate concerns:
+## Automatic Webhook Registration
-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`.
+If the service API supports programmatic webhook creation, implement `createSubscription` and `deleteSubscription` on the handler. The orchestration layer calls these automatically — **no code touches `route.ts`, `provider-subscriptions.ts`, or `deploy.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
+```typescript
+import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
+import type { DeleteSubscriptionContext, SubscriptionContext, SubscriptionResult } from '@/lib/webhooks/providers/types'
-- **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.
+export const {service}Handler: WebhookProviderHandler = {
+ async createSubscription(ctx: SubscriptionContext): Promise {
+ const config = getProviderConfig(ctx.webhook)
+ const apiKey = config.apiKey as string
+ if (!apiKey) throw new Error('{Service} API Key is required.')
-### Adding a Handler
+ const res = await fetch('https://api.{service}.com/webhooks', {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url: getNotificationUrl(ctx.webhook) }),
+ })
-In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block:
+ if (!res.ok) throw new Error(`{Service} error: ${res.status}`)
+ const { id } = (await res.json()) as { id: string }
+ return { providerConfigUpdates: { externalId: id } }
+ },
-```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,
- }
+ async deleteSubscription(ctx: DeleteSubscriptionContext): Promise {
+ const config = getProviderConfig(ctx.webhook)
+ const { apiKey, externalId } = config as { apiKey?: string; externalId?: string }
+ if (!apiKey || !externalId) return
+ await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
+ method: 'DELETE',
+ headers: { Authorization: `Bearer ${apiKey}` },
+ }).catch(() => {})
+ },
}
```
-**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
+**Key points:**
+- Throw from `createSubscription` — orchestration rolls back the DB webhook
+- Never throw from `deleteSubscription` — log non-fatally
+- Return `{ providerConfigUpdates: { externalId } }` — orchestration merges into `providerConfig`
+- Add `apiKey` field to `build{Service}ExtraFields` with `password: true`
-### Verify Alignment
-
-Run the alignment checker:
-```bash
-bunx scripts/check-trigger-alignment.ts {service}
-```
-
-## Trigger Outputs
+## Trigger Outputs Schema
Trigger outputs use the same schema as block outputs (NOT tool outputs).
-**Supported:**
-- `type` and `description` for simple fields
-- Nested object structure for complex data
-
-**NOT Supported:**
-- `optional: true` (tool outputs only)
-- `items` property (tool outputs only)
+**Supported:** `type` + `description` for leaf fields, nested objects for complex data.
+**NOT supported:** `optional: true`, `items` (those are tool-output-only features).
```typescript
export function buildOutputs(): Record {
return {
- // Simple fields
eventType: { type: 'string', description: 'Event type' },
timestamp: { type: 'string', description: 'When it occurred' },
-
- // Complex data - use type: 'json'
payload: { type: 'json', description: 'Full event payload' },
-
- // Nested structure
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
@@ -630,79 +327,32 @@ export function buildOutputs(): Record {
}
```
-## Generic Webhook Trigger Pattern
-
-For services with many event types, create a generic webhook that accepts all events:
-
-```typescript
-export const {service}WebhookTrigger: TriggerConfig = {
- id: '{service}_webhook',
- name: '{Service} Webhook (All Events)',
- // ...
-
- subBlocks: buildTriggerSubBlocks({
- triggerId: '{service}_webhook',
- triggerOptions: {service}TriggerOptions,
- setupInstructions: {service}SetupInstructions('All Events'),
- extraFields: [
- // Event type filter (optional)
- {
- id: 'eventTypes',
- title: 'Event Types',
- type: 'dropdown',
- multiSelect: true,
- options: [
- { label: 'Event A', id: 'event_a' },
- { label: 'Event B', id: 'event_b' },
- ],
- placeholder: 'Leave empty for all events',
- mode: 'trigger',
- condition: { field: 'selectedTriggerId', value: '{service}_webhook' },
- },
- // Plus any other service-specific fields
- ...build{Service}ExtraFields('{service}_webhook'),
- ],
- }),
-}
-```
-
-## Checklist Before Finishing
-
-### Utils
-- [ ] Created `{service}TriggerOptions` array with all trigger IDs
-- [ ] Created `{service}SetupInstructions` function with clear steps
-- [ ] Created `build{Service}ExtraFields` for service-specific fields
-- [ ] Created output builders for each trigger type
+## Checklist
-### Triggers
-- [ ] Primary trigger has `includeDropdown: true`
-- [ ] Secondary triggers do NOT have `includeDropdown`
+### Trigger Definition
+- [ ] Created `utils.ts` with options, instructions, extra fields, and output builders
+- [ ] Primary trigger has `includeDropdown: true`; secondary triggers do NOT
- [ ] All triggers use `buildTriggerSubBlocks` helper
-- [ ] All triggers have proper outputs defined
- [ ] Created `index.ts` barrel export
### Registration
-- [ ] All triggers imported in `triggers/registry.ts`
-- [ ] All triggers added to `TRIGGER_REGISTRY`
-- [ ] Block has `triggers.enabled: true`
-- [ ] Block has all trigger IDs in `triggers.available`
+- [ ] All triggers in `triggers/registry.ts` → `TRIGGER_REGISTRY`
+- [ ] Block has `triggers.enabled: true` and lists all trigger IDs in `triggers.available`
- [ ] Block spreads all trigger subBlocks: `...getTrigger('id').subBlocks`
-### Automatic Webhook Registration (if supported)
-- [ ] Added API key field to `build{Service}ExtraFields` with `password: true`
-- [ ] Updated setup instructions for automatic webhook creation
-- [ ] Added provider-specific logic to `apps/sim/app/api/webhooks/route.ts`
-- [ ] Added `create{Service}WebhookSubscription` helper function
-- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
-- [ ] Added provider to `cleanupExternalWebhook` function
+### Provider Handler (if needed)
+- [ ] Handler file at `apps/sim/lib/webhooks/providers/{service}.ts`
+- [ ] Registered in `providers/registry.ts` (alphabetical)
+- [ ] Signature validator is a private function inside the handler file
+- [ ] `formatInput` output keys match trigger `outputs` exactly
+- [ ] Event matching uses dynamic `await import()` for trigger utils
-### 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
+### Auto Registration (if supported)
+- [ ] `createSubscription` and `deleteSubscription` on the handler
+- [ ] NO changes to `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
+- [ ] API key field uses `password: true`
### Testing
-- [ ] Run `bun run type-check` to verify no TypeScript errors
-- [ ] Restart dev server to pick up new triggers
-- [ ] Test trigger UI shows correctly in the block
-- [ ] Test automatic webhook creation works (if applicable)
+- [ ] `bun run type-check` passes
+- [ ] Manually verify `formatInput` output keys match trigger `outputs` keys
+- [ ] Trigger UI shows correctly in the block
diff --git a/.claude/commands/validate-trigger.md b/.claude/commands/validate-trigger.md
new file mode 100644
index 00000000000..04bdc63c397
--- /dev/null
+++ b/.claude/commands/validate-trigger.md
@@ -0,0 +1,212 @@
+---
+description: Validate an existing Sim webhook trigger against provider API docs and repository conventions
+argument-hint: [api-docs-url]
+---
+
+# Validate Trigger
+
+You are an expert auditor for Sim webhook triggers. Your job is to validate that an existing trigger implementation is correct, complete, secure, and aligned across all layers.
+
+## Your Task
+
+1. Read the service's webhook/API documentation (via WebFetch)
+2. Read every trigger file, provider handler, and registry entry
+3. Cross-reference against the API docs and Sim conventions
+4. Report all issues grouped by severity (critical, warning, suggestion)
+5. Fix all issues after reporting them
+
+## Step 1: Gather All Files
+
+Read **every** file for the trigger — do not skip any:
+
+```
+apps/sim/triggers/{service}/ # All trigger files, utils.ts, index.ts
+apps/sim/lib/webhooks/providers/{service}.ts # Provider handler (if exists)
+apps/sim/lib/webhooks/providers/registry.ts # Handler registry
+apps/sim/triggers/registry.ts # Trigger registry
+apps/sim/blocks/blocks/{service}.ts # Block definition (trigger wiring)
+```
+
+Also read for reference:
+```
+apps/sim/lib/webhooks/providers/types.ts # WebhookProviderHandler interface
+apps/sim/lib/webhooks/providers/utils.ts # Shared helpers (createHmacVerifier, etc.)
+apps/sim/lib/webhooks/provider-subscription-utils.ts # Subscription helpers
+apps/sim/lib/webhooks/processor.ts # Central webhook processor
+```
+
+## Step 2: Pull API Documentation
+
+Fetch the service's official webhook documentation. This is the **source of truth** for:
+- Webhook event types and payload shapes
+- Signature/auth verification method (HMAC algorithm, header names, secret format)
+- Challenge/verification handshake requirements
+- Webhook subscription API (create/delete endpoints, if applicable)
+- Retry behavior and delivery guarantees
+
+## Step 3: Validate Trigger Definitions
+
+### utils.ts
+- [ ] `{service}TriggerOptions` lists all trigger IDs accurately
+- [ ] `{service}SetupInstructions` provides clear, correct steps for the service
+- [ ] `build{Service}ExtraFields` includes relevant filter/config fields with correct `condition`
+- [ ] Output builders expose all meaningful fields from the webhook payload
+- [ ] Output builders do NOT use `optional: true` or `items` (tool-output-only features)
+- [ ] Nested output objects correctly model the payload structure
+
+### Trigger Files
+- [ ] Exactly one primary trigger has `includeDropdown: true`
+- [ ] All secondary triggers do NOT have `includeDropdown`
+- [ ] All triggers use `buildTriggerSubBlocks` helper (not hand-rolled subBlocks)
+- [ ] Every trigger's `id` matches the convention `{service}_{event_name}`
+- [ ] Every trigger's `provider` matches the service name used in the handler registry
+- [ ] `index.ts` barrel exports all triggers
+
+### Trigger ↔ Provider Alignment (CRITICAL)
+- [ ] Every trigger ID referenced in `matchEvent` logic exists in `{service}TriggerOptions`
+- [ ] Event matching logic in the provider correctly maps trigger IDs to service event types
+- [ ] Event matching logic in `is{Service}EventMatch` (if exists) correctly identifies events per the API docs
+
+## Step 4: Validate Provider Handler
+
+### Auth Verification
+- [ ] `verifyAuth` correctly validates webhook signatures per the service's documentation
+- [ ] HMAC algorithm matches (SHA-1, SHA-256, SHA-512)
+- [ ] Signature header name matches the API docs exactly
+- [ ] Signature format is handled (raw hex, `sha256=` prefix, base64, etc.)
+- [ ] Uses `safeCompare` for timing-safe comparison (no `===`)
+- [ ] If `webhookSecret` is required, handler rejects when it's missing (fail-closed)
+- [ ] Signature is computed over raw body (not parsed JSON)
+
+### Event Matching
+- [ ] `matchEvent` returns `boolean` (not `NextResponse` or other values)
+- [ ] Challenge/verification events are excluded from matching (e.g., `endpoint.url_validation`)
+- [ ] When `triggerId` is a generic webhook ID, all events pass through
+- [ ] When `triggerId` is specific, only matching events pass
+- [ ] Event matching logic uses dynamic `await import()` for trigger utils (avoids circular deps)
+
+### formatInput (CRITICAL)
+- [ ] Every key in the `formatInput` return matches a key in the trigger `outputs` schema
+- [ ] Every key in the trigger `outputs` schema is populated by `formatInput`
+- [ ] No extra undeclared keys that users can't discover in the UI
+- [ ] No wrapper objects (`webhook: { ... }`, `{service}: { ... }`)
+- [ ] Nested output paths exist at the correct depth (e.g., `resource.id` actually has `resource: { id: ... }`)
+- [ ] `null` is used for missing optional fields (not empty strings or empty objects)
+- [ ] Returns `{ input: { ... } }` — not a bare object
+
+### Idempotency
+- [ ] `extractIdempotencyId` returns a stable, unique key per delivery
+- [ ] Uses provider-specific delivery IDs when available (e.g., `X-Request-Id`, `Linear-Delivery`, `svix-id`)
+- [ ] Falls back to content-based ID (e.g., `${type}:${id}`) when no delivery header exists
+- [ ] Does NOT include timestamps in the idempotency key (would break dedup on retries)
+
+### Challenge Handling (if applicable)
+- [ ] `handleChallenge` correctly implements the service's URL verification handshake
+- [ ] Returns the expected response format per the API docs
+- [ ] Env-backed secrets are resolved via `resolveEnvVarsInObject` if needed
+
+## Step 5: Validate Automatic Subscription Lifecycle
+
+If the service supports programmatic webhook creation:
+
+### createSubscription
+- [ ] Calls the correct API endpoint to create a webhook
+- [ ] Sends the correct event types/filters
+- [ ] Passes the notification URL from `getNotificationUrl(ctx.webhook)`
+- [ ] Returns `{ providerConfigUpdates: { externalId } }` with the external webhook ID
+- [ ] Throws on failure (orchestration handles rollback)
+- [ ] Provides user-friendly error messages (401 → "Invalid API Key", etc.)
+
+### deleteSubscription
+- [ ] Calls the correct API endpoint to delete the webhook
+- [ ] Handles 404 gracefully (webhook already deleted)
+- [ ] Never throws — catches errors and logs non-fatally
+- [ ] Skips gracefully when `apiKey` or `externalId` is missing
+
+### Orchestration Isolation
+- [ ] NO provider-specific logic in `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
+- [ ] All subscription logic lives on the handler (`createSubscription`/`deleteSubscription`)
+
+## Step 6: Validate Registration and Block Wiring
+
+### Trigger Registry (`triggers/registry.ts`)
+- [ ] All triggers are imported and registered
+- [ ] Registry keys match trigger IDs exactly
+- [ ] No orphaned entries (triggers that don't exist)
+
+### Provider Handler Registry (`providers/registry.ts`)
+- [ ] Handler is imported and registered (if handler exists)
+- [ ] Registry key matches the `provider` field on the trigger configs
+- [ ] Entries are in alphabetical order
+
+### Block Wiring (`blocks/blocks/{service}.ts`)
+- [ ] Block has `triggers.enabled: true`
+- [ ] `triggers.available` lists all trigger IDs
+- [ ] All trigger subBlocks are spread into `subBlocks`: `...getTrigger('id').subBlocks`
+- [ ] No trigger IDs in `triggers.available` that aren't in the registry
+- [ ] No trigger subBlocks spread that aren't in `triggers.available`
+
+## Step 7: Validate Security
+
+- [ ] Webhook secrets are never logged (not even at debug level)
+- [ ] Auth verification runs before any event processing
+- [ ] No secret comparison uses `===` (must use `safeCompare` or `crypto.timingSafeEqual`)
+- [ ] Timestamp/replay protection is reasonable (not too tight for retries, not too loose for security)
+- [ ] Raw body is used for signature verification (not re-serialized JSON)
+
+## Step 8: Report and Fix
+
+### Report Format
+
+Group findings by severity:
+
+**Critical** (runtime errors, security issues, or data loss):
+- Wrong HMAC algorithm or header name
+- `formatInput` keys don't match trigger `outputs`
+- Missing `verifyAuth` when the service sends signed webhooks
+- `matchEvent` returns non-boolean values
+- Provider-specific logic leaking into shared orchestration files
+- Trigger IDs mismatch between trigger files, registry, and block
+- `createSubscription` calling wrong API endpoint
+- Auth comparison using `===` instead of `safeCompare`
+
+**Warning** (convention violations or usability issues):
+- Missing `extractIdempotencyId` when the service provides delivery IDs
+- Timestamps in idempotency keys (breaks dedup on retries)
+- Missing challenge handling when the service requires URL verification
+- Output schema missing fields that `formatInput` returns (undiscoverable data)
+- Overly tight timestamp skew window that rejects legitimate retries
+- `matchEvent` not filtering challenge/verification events
+- Setup instructions missing important steps
+
+**Suggestion** (minor improvements):
+- More specific output field descriptions
+- Additional output fields that could be exposed
+- Better error messages in `createSubscription`
+- Logging improvements
+
+### Fix All Issues
+
+After reporting, fix every **critical** and **warning** issue. Apply **suggestions** where they don't add unnecessary complexity.
+
+### Validation Output
+
+After fixing, confirm:
+1. `bun run type-check` passes
+2. Re-read all modified files to verify fixes are correct
+3. Provider handler tests pass (if they exist): `bun test {service}`
+
+## Checklist Summary
+
+- [ ] Read all trigger files, provider handler, types, registries, and block
+- [ ] Pulled and read official webhook/API documentation
+- [ ] Validated trigger definitions: options, instructions, extra fields, outputs
+- [ ] Validated primary/secondary trigger distinction (`includeDropdown`)
+- [ ] Validated provider handler: auth, matchEvent, formatInput, idempotency
+- [ ] Validated output alignment: every `outputs` key ↔ every `formatInput` key
+- [ ] Validated subscription lifecycle: createSubscription, deleteSubscription, no shared-file edits
+- [ ] Validated registration: trigger registry, handler registry, block wiring
+- [ ] Validated security: safe comparison, no secret logging, replay protection
+- [ ] Reported all issues grouped by severity
+- [ ] Fixed all critical and warning issues
+- [ ] `bun run type-check` passes after fixes
diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx
index 6a2a66183fb..cbc3b5edd85 100644
--- a/apps/docs/components/icons.tsx
+++ b/apps/docs/components/icons.tsx
@@ -2132,7 +2132,15 @@ export function Mem0Icon(props: SVGProps) {
export function ExtendIcon(props: SVGProps) {
return (
-