Skip to content

Commit 34e1bd7

Browse files
waleedlatif1claude
andcommitted
feat(trigger): add Google Sheets, Drive, and Calendar polling triggers
Add polling triggers for Google Sheets (new rows), Google Drive (file changes via changes.list API), and Google Calendar (event updates via updatedMin). Each includes OAuth credential support, configurable filters (event type, MIME type, folder, search term, render options), idempotency, and first-poll seeding. Wire triggers into block configs and regenerate integrations.json. Update add-trigger skill with polling instructions and versioned block wiring guidance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c393791 commit 34e1bd7

19 files changed

Lines changed: 2114 additions & 26 deletions

File tree

.claude/commands/add-trigger.md

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
---
2-
description: Create webhook triggers for a Sim integration using the generic trigger builder
2+
description: Create webhook or polling triggers for a Sim integration
33
argument-hint: <service-name>
44
---
55

66
# Add Trigger
77

8-
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.
8+
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.
99

1010
## Your Task
1111

12-
1. Research what webhook events the service supports
13-
2. Create the trigger files using the generic builder
14-
3. Create a provider handler if custom auth, formatting, or subscriptions are needed
12+
1. Research what webhook events the service supports — if the service lacks reliable webhooks, use polling
13+
2. Create the trigger files using the generic builder (webhook) or manual config (polling)
14+
3. Create a provider handler (webhook) or polling handler (polling)
1515
4. Register triggers and connect them to the block
1616

1717
## Directory Structure
@@ -146,23 +146,37 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
146146

147147
### Block file (`apps/sim/blocks/blocks/{service}.ts`)
148148

149+
Wire triggers into the block so the trigger UI appears and `generate-docs.ts` discovers them. Two changes are needed:
150+
151+
1. **Spread trigger subBlocks** at the end of the block's `subBlocks` array
152+
2. **Add `triggers` property** after `outputs` with `enabled: true` and `available: [...]`
153+
149154
```typescript
150155
import { getTrigger } from '@/triggers'
151156

152157
export const {Service}Block: BlockConfig = {
153158
// ...
154-
triggers: {
155-
enabled: true,
156-
available: ['{service}_event_a', '{service}_event_b'],
157-
},
158159
subBlocks: [
159160
// Regular tool subBlocks first...
160161
...getTrigger('{service}_event_a').subBlocks,
161162
...getTrigger('{service}_event_b').subBlocks,
162163
],
164+
// ... tools, inputs, outputs ...
165+
triggers: {
166+
enabled: true,
167+
available: ['{service}_event_a', '{service}_event_b'],
168+
},
163169
}
164170
```
165171

172+
**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:
173+
174+
- **V2 uses `...V1Block` spread** (e.g., Google Calendar): Add trigger to V1 — V2 inherits both `subBlocks` and `triggers` automatically.
175+
- **V2 defines its own `subBlocks`** (e.g., Google Sheets): Add trigger to V2 (the visible block). V1 is hidden and doesn't need it.
176+
- **Single block, no V2** (e.g., Google Drive): Add trigger directly.
177+
178+
`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.
179+
166180
## Provider Handler
167181

168182
All provider-specific webhook logic lives in a single handler file: `apps/sim/lib/webhooks/providers/{service}.ts`.
@@ -327,6 +341,122 @@ export function buildOutputs(): Record<string, TriggerOutput> {
327341
}
328342
```
329343

344+
## Polling Triggers
345+
346+
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.
347+
348+
### Directory Structure
349+
350+
```
351+
apps/sim/triggers/{service}/
352+
├── index.ts # Barrel export
353+
└── poller.ts # TriggerConfig with polling: true
354+
355+
apps/sim/lib/webhooks/polling/
356+
└── {service}.ts # PollingProviderHandler implementation
357+
```
358+
359+
### Polling Handler (`apps/sim/lib/webhooks/polling/{service}.ts`)
360+
361+
```typescript
362+
import { pollingIdempotency } from '@/lib/core/idempotency/service'
363+
import type { PollingProviderHandler, PollWebhookContext } from '@/lib/webhooks/polling/types'
364+
import { markWebhookFailed, markWebhookSuccess, resolveOAuthCredential, updateWebhookProviderConfig } from '@/lib/webhooks/polling/utils'
365+
import { processPolledWebhookEvent } from '@/lib/webhooks/processor'
366+
367+
export const {service}PollingHandler: PollingProviderHandler = {
368+
provider: '{service}',
369+
label: '{Service}',
370+
371+
async pollWebhook(ctx: PollWebhookContext): Promise<'success' | 'failure'> {
372+
const { webhookData, workflowData, requestId, logger } = ctx
373+
const webhookId = webhookData.id
374+
375+
try {
376+
// For OAuth services:
377+
const accessToken = await resolveOAuthCredential(webhookData, '{service}', requestId, logger)
378+
const config = webhookData.providerConfig as unknown as {Service}WebhookConfig
379+
380+
// First poll: seed state, emit nothing
381+
if (!config.lastCheckedTimestamp) {
382+
await updateWebhookProviderConfig(webhookId, { lastCheckedTimestamp: new Date().toISOString() }, logger)
383+
await markWebhookSuccess(webhookId, logger)
384+
return 'success'
385+
}
386+
387+
// Fetch changes since last poll, process with idempotency
388+
// ...
389+
390+
await markWebhookSuccess(webhookId, logger)
391+
return 'success'
392+
} catch (error) {
393+
logger.error(`[${requestId}] Error processing {service} webhook ${webhookId}:`, error)
394+
await markWebhookFailed(webhookId, logger)
395+
return 'failure'
396+
}
397+
},
398+
}
399+
```
400+
401+
**Key patterns:**
402+
- First poll seeds state and emits nothing (avoids flooding with existing data)
403+
- Use `pollingIdempotency.executeWithIdempotency(provider, key, callback)` for dedup
404+
- Use `processPolledWebhookEvent(webhookData, workflowData, payload, requestId)` to fire the workflow
405+
- Use `updateWebhookProviderConfig(webhookId, partialConfig, logger)` for read-merge-write on state
406+
- Use the latest server-side timestamp from API responses (not wall clock) to avoid clock skew
407+
408+
### Trigger Config (`apps/sim/triggers/{service}/poller.ts`)
409+
410+
```typescript
411+
import { {Service}Icon } from '@/components/icons'
412+
import type { TriggerConfig } from '@/triggers/types'
413+
414+
export const {service}PollingTrigger: TriggerConfig = {
415+
id: '{service}_poller',
416+
name: '{Service} Trigger',
417+
provider: '{service}',
418+
description: 'Triggers when ...',
419+
version: '1.0.0',
420+
icon: {Service}Icon,
421+
polling: true, // REQUIRED — routes to polling infrastructure
422+
423+
subBlocks: [
424+
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true },
425+
// ... service-specific config fields (dropdowns, inputs, switches) ...
426+
{ id: 'triggerSave', type: 'trigger-save', title: '', hideFromPreview: true, mode: 'trigger', triggerId: '{service}_poller' },
427+
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' },
428+
],
429+
430+
outputs: {
431+
// Must match the payload shape from processPolledWebhookEvent
432+
},
433+
}
434+
```
435+
436+
### Registration (3 places)
437+
438+
1. **`apps/sim/triggers/constants.ts`** — add provider to `POLLING_PROVIDERS` Set
439+
2. **`apps/sim/lib/webhooks/polling/registry.ts`** — import handler, add to `POLLING_HANDLERS`
440+
3. **`apps/sim/triggers/registry.ts`** — import trigger config, add to `TRIGGER_REGISTRY`
441+
442+
### Helm Cron Job
443+
444+
Add to `helm/sim/values.yaml` under the existing polling cron jobs:
445+
446+
```yaml
447+
{service}WebhookPoll:
448+
schedule: "*/1 * * * *"
449+
concurrencyPolicy: Forbid
450+
url: "http://sim:3000/api/webhooks/poll/{service}"
451+
```
452+
453+
### Reference Implementations
454+
455+
- Simple: `apps/sim/lib/webhooks/polling/rss.ts` + `apps/sim/triggers/rss/poller.ts`
456+
- Complex (OAuth, attachments): `apps/sim/lib/webhooks/polling/gmail.ts` + `apps/sim/triggers/gmail/poller.ts`
457+
- Cursor-based (changes API): `apps/sim/lib/webhooks/polling/google-drive.ts`
458+
- Timestamp-based: `apps/sim/lib/webhooks/polling/google-calendar.ts`
459+
330460
## Checklist
331461

332462
### Trigger Definition
@@ -352,7 +482,18 @@ export function buildOutputs(): Record<string, TriggerOutput> {
352482
- [ ] NO changes to `route.ts`, `provider-subscriptions.ts`, or `deploy.ts`
353483
- [ ] API key field uses `password: true`
354484

485+
### Polling Trigger (if applicable)
486+
- [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts`
487+
- [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`)
488+
- [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry
489+
- [ ] `triggerSave` subBlock `triggerId` matches trigger config `id`
490+
- [ ] First poll seeds state and emits nothing
491+
- [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts`
492+
- [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts`
493+
- [ ] Added cron job to `helm/sim/values.yaml`
494+
- [ ] Payload shape matches trigger `outputs` schema
495+
355496
### Testing
356497
- [ ] `bun run type-check` passes
357-
- [ ] Manually verify `formatInput` output keys match trigger `outputs` keys
498+
- [ ] Manually verify output keys match trigger `outputs` keys
358499
- [ ] Trigger UI shows correctly in the block

0 commit comments

Comments
 (0)