| name | add-trigger |
| description | Create webhook or polling triggers for a Sim integration |
| argument-hint | <service-name> |
Add Trigger
You are an expert at creating webhook and polling triggers for Sim. You understand the trigger system, the generic buildTriggerSubBlocks helper, polling infrastructure, and how triggers connect to blocks.
Your Task
- Research what webhook events the service supports — if the service lacks reliable webhooks, use polling
- Create the trigger files using the generic builder (webhook) or manual config (polling)
- Create a provider handler (webhook) or polling handler (polling)
- Register triggers and connect them to the block
Hard Rule: No Guessed Webhook Payload Schemas
If the service docs do not clearly show the webhook payload JSON for an event, you MUST tell the user instead of guessing trigger outputs or formatInput mappings.
- Do NOT invent payload field names
- Do NOT guess nested event object paths
- Do NOT infer output fields from the UI or marketing docs
- Do NOT write
formatInput against unverified webhook bodies
If the payload shape is unknown, do one of these instead:
- Ask the user for sample webhook payloads
- Ask the user for a test webhook source so you can inspect a real event
- Implement only the event registration/setup portions whose payloads are documented
- Leave the trigger unimplemented and explicitly say which payload fields are unknown
Directory Structure
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
├── utils.ts # Service-specific helpers (options, instructions, extra fields, outputs)
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.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
This file contains all service-specific helpers used by triggers.
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
export const {service}TriggerOptions = [
{ label: 'Event A', id: '{service}_event_a' },
{ label: 'Event B', id: '{service}_event_b' },
]
export function {service}SetupInstructions(eventType: string): string {
const instructions = [
'Copy the <strong>Webhook URL</strong> above',
'Go to <strong>{Service} Settings > Webhooks</strong>',
`Select the <strong>${eventType}</strong> event type`,
'Paste the webhook URL and save',
'Click "Save" above to activate your trigger',
]
return instructions
.map((instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
export function build{Service}ExtraFields(triggerId: ): [] {
[
{
: ,
: ,
: ,
: ,
: ,
: { : , : triggerId },
},
]
}
build{}(): <, > {
{
: { : , : },
: { : , : },
: {
: { : , : },
: { : , : },
},
}
}
Step 2: Create Trigger Files
Primary trigger — MUST include includeDropdown: true:
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'
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
name: '{Service} Event A',
provider: '{service}',
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,
setupInstructions: {service}SetupInstructions('Event A'),
: build{}(),
}),
: build{}(),
: { : , : { : } },
}
Secondary triggers — NO includeDropdown (it's already in the primary):
export const {service}EventBTrigger: TriggerConfig = {
}
Step 3: Register and Wire
apps/sim/triggers/{service}/index.ts
export { {service}EventATrigger } from './event_a'
export { {service}EventBTrigger } from './event_b'
apps/sim/triggers/registry.ts
import { {service}EventATrigger, {service}EventBTrigger } from '@/triggers/{service}'
export const TRIGGER_REGISTRY: TriggerRegistry = {
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
}
Block file (apps/sim/blocks/blocks/{service}.ts)
Wire triggers into the block so the trigger UI appears and generate-docs.ts discovers them. Two changes are needed:
- Spread trigger subBlocks at the end of the block's
subBlocks array
- Add
triggers property after outputs with enabled: true and available: [...]
import { getTrigger } from '@/triggers'
export const {Service}Block: BlockConfig = {
subBlocks: [
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
],
triggers: {
enabled: true,
available: ['{service}_event_a', '{service}_event_b'],
},
}
Versioned blocks (V1 + V2): Many integrations have a hidden V1 block and a visible V2 block. Where you add the trigger wiring depends on how V2 inherits from V1:
- V2 uses
...V1Block spread (e.g., Google Calendar): Add trigger to V1 — V2 inherits both subBlocks and triggers automatically.
- V2 defines its own
subBlocks (e.g., Google Sheets): Add trigger to V2 (the visible block). V1 is hidden and doesn't need it.
- Single block, no V2 (e.g., Google Drive): Add trigger directly.
generate-docs.ts deduplicates by base type (first match wins). If V1 is processed first without triggers, the V2 triggers won't appear in integrations.json. Always verify by checking the output after running the script.
Provider Handler
All provider-specific webhook logic lives in a single handler file: apps/sim/lib/webhooks/providers/{service}.ts.
When to Create a Handler
| 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 |
If none apply, you don't need a handler. The default handler provides bearer token auth.
Example Handler
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)
}
export const {service}Handler: WebhookProviderHandler = {
: ({
: ,
: ,
: validate{},
: ,
}),
() {
triggerId = providerConfig. |
(triggerId && triggerId !== ) {
{ is{} } = ()
(!is{}(triggerId, body <, >))
}
},
({ body }: ): <> {
b = body <, >
{
: {
: b.,
: (b. <, >)?. || ,
: b.,
},
}
},
() {
obj = body <, >
obj. && obj. ? :
},
}
Register the Handler
In apps/sim/lib/webhooks/providers/registry.ts:
import { {service}Handler } from '@/lib/webhooks/providers/{service}'
const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
{service}: {service}Handler,
}
Output Alignment (Critical)
There are two sources of truth that MUST be aligned:
- Trigger
outputs — schema defining what fields SHOULD be available (UI tag dropdown)
formatInput on the handler — implementation that transforms raw payload into actual data
If they differ: the tag dropdown shows fields that don't exist, or actual data has fields users can't discover.
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
Automatic Webhook Registration
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.
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
import type { DeleteSubscriptionContext, SubscriptionContext, SubscriptionResult } from '@/lib/webhooks/providers/types'
export const {service}Handler: WebhookProviderHandler = {
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
const config = getProviderConfig(ctx.webhook)
const apiKey = config.apiKey as string
if (!apiKey) throw new Error('{Service} API Key is required.')
const res = await fetch('https://api.{service}.com/webhooks', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: (ctx.) }),
})
(!res.) ()
{ id } = ( res.()) { : }
{ : { : id } }
},
(: ): <> {
config = (ctx.)
{ apiKey, externalId } = config { ?: ; ?: }
(!apiKey || !externalId)
(, {
: ,
: { : },
}).( {})
},
}
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
Trigger Outputs Schema
Trigger outputs use the same schema as block outputs (NOT tool outputs).
Supported: type + description for leaf fields, nested objects for complex data.
NOT supported: optional: true, items (those are tool-output-only features).
export function buildOutputs(): Record<string, TriggerOutput> {
return {
eventType: { type: 'string', description: 'Event type' },
timestamp: { type: 'string', description: 'When it occurred' },
payload: { type: 'json', description: 'Full event payload' },
resource: {
id: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
},
}
}
Polling Triggers
Use polling when the service lacks reliable webhooks (e.g., Google Sheets, Google Drive, Google Calendar, Gmail, RSS, IMAP). Polling triggers do NOT use buildTriggerSubBlocks — they define subBlocks manually.
Directory Structure
apps/sim/triggers/{service}/
├── index.ts # Barrel export
└── poller.ts # TriggerConfig with polling: true
apps/sim/lib/webhooks/polling/
└── {service}.ts # PollingProviderHandler implementation
Polling Handler (apps/sim/lib/webhooks/polling/{service}.ts)
import { pollingIdempotency } from '@/lib/core/idempotency/service'
import type { PollingProviderHandler, PollWebhookContext } from '@/lib/webhooks/polling/types'
import { markWebhookFailed, markWebhookSuccess, resolveOAuthCredential, updateWebhookProviderConfig } from '@/lib/webhooks/polling/utils'
import { processPolledWebhookEvent } from '@/lib/webhooks/processor'
export const {service}PollingHandler: PollingProviderHandler = {
provider: '{service}',
label: '{Service}',
async pollWebhook(ctx: PollWebhookContext): Promise<'success' | 'failure'> {
const { webhookData, workflowData, requestId, logger } = ctx
const webhookId = webhookData.id
try {
const accessToken = await resolveOAuthCredential(webhookData, '{service}', requestId)
const config = webhookData.providerConfig as unknown as {Service}WebhookConfig
(!config.) {
(webhookId, { : ().() }, logger)
(webhookId, logger)
}
(webhookId, logger)
} (error) {
logger.(, error)
(webhookId, logger)
}
},
}
Key patterns:
- First poll seeds state and emits nothing (avoids flooding with existing data)
- Use
pollingIdempotency.executeWithIdempotency(provider, key, callback) for dedup
- Use
processPolledWebhookEvent(webhookData, workflowData, payload, requestId) to fire the workflow
- Use
updateWebhookProviderConfig(webhookId, partialConfig, logger) for read-merge-write on state
- Use the latest server-side timestamp from API responses (not wall clock) to avoid clock skew
Trigger Config (apps/sim/triggers/{service}/poller.ts)
import { {Service}Icon } from '@/components/icons'
import type { TriggerConfig } from '@/triggers/types'
export const {service}PollingTrigger: TriggerConfig = {
id: '{service}_poller',
name: '{Service} Trigger',
provider: '{service}',
description: 'Triggers when ...',
version: '1.0.0',
icon: {Service}Icon,
polling: true,
subBlocks: [
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger' },
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: , : , : },
],
: {
},
}
Registration (3 places)
apps/sim/triggers/constants.ts — add provider to POLLING_PROVIDERS Set
apps/sim/lib/webhooks/polling/registry.ts — import handler, add to POLLING_HANDLERS
apps/sim/triggers/registry.ts — import trigger config, add to TRIGGER_REGISTRY
Helm Cron Job
Add to helm/sim/values.yaml under the existing polling cron jobs:
{service}WebhookPoll:
schedule: "*/1 * * * *"
concurrencyPolicy: Forbid
url: "http://sim:3000/api/webhooks/poll/{service}"
Reference Implementations
- Simple:
apps/sim/lib/webhooks/polling/rss.ts + apps/sim/triggers/rss/poller.ts
- Complex (OAuth, attachments):
apps/sim/lib/webhooks/polling/gmail.ts + apps/sim/triggers/gmail/poller.ts
- Cursor-based (changes API):
apps/sim/lib/webhooks/polling/google-drive.ts
- Timestamp-based:
apps/sim/lib/webhooks/polling/google-calendar.ts
Checklist
Trigger Definition
Registration
Provider Handler (if needed)
Auto Registration (if supported)
Polling Trigger (if applicable)
Testing