Skip to content

Commit 6099683

Browse files
waleedlatif1claude
andauthored
feat(trigger): add Google Sheets, Drive, and Calendar polling triggers (#4081)
* 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> * fix(polling): address PR review feedback for Google polling triggers - Fix Drive cursor stall: use nextPageToken as resume point when breaking early from pagination instead of re-using the original token - Eliminate redundant Drive API call in Sheets poller by returning modifiedTime from the pre-check function - Add 403/429 rate-limit handling to Sheets API calls matching the Calendar handler pattern - Remove unused changeType field from DriveChangeEntry interface - Rename triggers/google_drive to triggers/google-drive for consistency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(polling): fix Drive pre-check never activating in Sheets poller isDriveFileUnchanged short-circuited when lastModifiedTime was undefined, never calling the Drive API — so currentModifiedTime was never populated, creating a permanent chicken-and-egg loop. Now always calls the Drive API and returns the modifiedTime regardless of whether there's a previous value to compare against. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(lint): fix import ordering in triggers registry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(polling): address PR review feedback for Google polling handlers - Fix fetchHeaderRow to throw on 403/429 rate limits instead of silently returning empty headers (prevents rows from being processed without headers and lastKnownRowCount from advancing past them permanently) - Fix Drive pagination to avoid advancing resume cursor past sliced changes (prevents permanent change loss when allChanges > maxFiles) - Remove unused logger import from Google Drive trigger config * fix(polling): prevent data loss on partial row failures and harden idempotency key - Sheets: only advance lastKnownRowCount by processedCount when there are failures, so failed rows are retried on the next poll cycle (idempotency deduplicates already-processed rows on re-fetch) - Drive: add fallback for change.time in idempotency key to prevent key collisions if the field is ever absent from the API response * fix(polling): remove unused variable and preserve lastModifiedTime on Drive API failure - Remove unused `now` variable from Google Drive polling handler - Preserve stored lastModifiedTime when Drive API pre-check fails (previously wrote undefined, disabling the optimization until the next successful Drive API call) * fix(polling): don't advance state when all events fail across sheets, calendar, drive handlers * fix(polling): retry failed idempotency keys, fix drive cursor overshoot, fix calendar inclusive updatedMin * fix(polling): revert calendar timestamp on any failure, not just all-fail * fix(polling): revert drive cursor on any failure, not just all-fail * feat(triggers): add canonical selector toggle to google polling triggers - Add 'trigger-advanced' mode to SubBlockConfig so canonical pairs work in trigger mode - Fix buildCanonicalIndex: trigger-mode subblocks don't overwrite non-trigger basicId, deduplicate advancedIds from block spreads - Update editor, subblock layout, and trigger config aggregation to include trigger-advanced subblocks - Replace dropdown+fetchOptions in Calendar/Sheets/Drive pollers with file-selector (basic) + short-input (advanced) canonical pairs - Add canonicalParamId: 'oauthCredential' to triggerCredentials for selector context resolution - Update polling handlers to read canonical fallbacks (calendarId||manualCalendarId, etc.) * test(blocks): handle trigger-advanced mode in canonical validation tests * fix(triggers): handle trigger-advanced mode in deploy, preview, params, and copilot * fix(polling): use position-only idempotency key for sheets rows * fix(polling): don't advance calendar timestamp to client clock on empty poll * fix(polling): remove extraneous comment from calendar poller * fix(polling): drive cursor stall on full page, calendar latestUpdated past filtered events * fix(polling): advance calendar cursor past fully-filtered event batches --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3efbd1d commit 6099683

File tree

32 files changed

+2216
-59
lines changed

32 files changed

+2216
-59
lines changed

.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)