Skip to content

feat(trigger): add Google Sheets, Drive, and Calendar polling triggers#4081

Open
waleedlatif1 wants to merge 17 commits intostagingfrom
waleedlatif1/polling-research
Open

feat(trigger): add Google Sheets, Drive, and Calendar polling triggers#4081
waleedlatif1 wants to merge 17 commits intostagingfrom
waleedlatif1/polling-research

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented Apr 9, 2026

Summary

  • Add polling triggers for Google Sheets (new rows), Google Drive (file changes), and Google Calendar (event updates)
  • Google Drive uses changes.list API with opaque cursor (no clock skew, catches deletes)
  • Google Calendar uses updatedMin + singleEvents=true (expanded recurring events)
  • Google Sheets uses row count comparison + Drive modifiedTime pre-check (saves Sheets quota)
  • Each poller supports: OAuth credentials, configurable filters (event type, MIME type, folder, search term, render options), idempotency, first-poll seeding
  • Wire triggers into block configs so trigger UI appears and generate-docs.ts discovers them
  • Regenerate integrations.json with new trigger entries
  • Update add-trigger skill with polling instructions and versioned block wiring guidance
  • Added trigger-advanced mode for subblocks for advanced mode fields in trigger mode

Type of Change

  • New feature

Testing

Tested manually — type-check passes, lint passes, integrations.json regenerated correctly

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 10, 2026 2:17am

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 9, 2026

PR Summary

Medium Risk
Adds new polling-based trigger execution paths for Google Calendar/Drive/Sheets and changes idempotency behavior for polling to retry failures, which could affect webhook processing volume and duplicate prevention. UI/subblock mode filtering is broadened to include a new trigger-advanced mode, impacting how trigger configs render and are aggregated across the editor/preview/copilot metadata.

Overview
Adds polling triggers for Google Calendar, Google Drive, and Google Sheets, including new polling handlers under lib/webhooks/polling/ that fetch changes from Google APIs, seed initial state on first poll, apply user-configurable filters, and emit events via processPolledWebhookEvent with polling idempotency.

Wires these triggers into the corresponding Google blocks (spreading trigger subBlocks and enabling triggers.available), registers them in triggers/registry.ts and lib/webhooks/polling/registry.ts, adds providers to POLLING_PROVIDERS, and schedules new polling cronjobs in Helm; integrations.json is regenerated to expose the new triggers.

Introduces a new subBlock mode trigger-advanced and updates editor/preview/layout, trigger config aggregation/deploy, copilot block metadata extraction, visibility canonical grouping, and validation tests to treat both trigger and trigger-advanced as trigger-only fields. Updates the add-trigger docs/commands to cover polling triggers and versioned block trigger wiring.

Reviewed by Cursor Bugbot for commit ef82ce6. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 9, 2026

Greptile Summary

This PR adds three new polling triggers — Google Sheets (new row detection), Google Drive (file change tracking via cursor), and Google Calendar (event changes via updatedMin) — along with a new trigger-advanced mode for subblocks that pairs basic/advanced UI variants in trigger mode. The polling handlers, idempotency enhancements (retryFailures), Helm cron jobs, block wiring, and UI filtering are all included.

  • Drive cursor stalls silently: When the Drive changes API returns exactly maxFiles (50) changes and there are more pending, lastNextPageToken is never captured (the break guard if (hasMore && !overLimit) excludes this case). resumeToken falls back to the old cursor, so each poll re-fetches the same 50 changes as idempotency cache hits — new changes beyond those 50 are permanently invisible until the backlog drops below 50.

Confidence Score: 3/5

Not safe to merge as-is — the Drive cursor stall bug causes silent, permanent data loss in a plausible high-traffic scenario

One confirmed P1 logic bug in the Drive polling handler: when the first page returns exactly maxFiles (50) changes with more pending, lastNextPageToken is never captured and the cursor reverts to the old position on every poll. This silently prevents all subsequent Drive changes from being detected. The Calendar and Sheets handlers look correct after prior-round fixes, and the idempotency retryFailures enhancement is solid. The Drive cursor fix is small but is a blocking correctness issue.

apps/sim/lib/webhooks/polling/google-drive.ts — the fetchChanges cursor resolution logic (lines 260–278)

Important Files Changed

Filename Overview
apps/sim/lib/webhooks/polling/google-drive.ts New Drive polling handler — cursor stalls permanently when exactly maxFiles changes are returned with more pending (P1); also missing 403/429 rate-limit handling unlike Calendar/Sheets
apps/sim/lib/webhooks/polling/google-calendar.ts New Calendar polling handler — correctly preserves timestamp on any failure (failedCount > 0), has 403/429 handling, and uses latestUpdated+1ms to advance state
apps/sim/lib/webhooks/polling/google-sheets.ts New Sheets polling handler — Drive pre-check saves quota, row count logic is correct, 403/429 handling present in all three API call sites
apps/sim/lib/core/idempotency/service.ts Adds retryFailures option — deletes key on failure instead of storing it, enabling safe retry on next poll cycle; recursive retry path is bounded by poll cadence
apps/sim/lib/workflows/subblocks/visibility.ts Correctly handles trigger-advanced in canonical index building — deduplicates advancedIds and prevents trigger-mode subblocks from overwriting existing basicId
apps/sim/triggers/google-drive/poller.ts New Drive trigger config — subblocks, outputs, and folder/MIME type filter options look correct
apps/sim/triggers/google-calendar/poller.ts New Calendar trigger config — subblocks and output schema look complete and correct
apps/sim/triggers/google-sheets/poller.ts New Sheets trigger config — basic/advanced mode pairing for spreadsheetId and sheetName is properly set up with canonicalParamId
helm/sim/values.yaml Adds three new 1-minute cron jobs for Drive, Calendar, and Sheets polling with Forbid concurrency — consistent with existing polling job configuration

Sequence Diagram

sequenceDiagram
    participant Cron as Cron Job (1 min)
    participant Poll as Poll API Route
    participant Handler as Polling Handler
    participant Google as Google API
    participant Idem as Idempotency Service
    participant Proc as Webhook Processor

    Cron->>Poll: POST /api/webhooks/poll/{provider}
    Poll->>Handler: pollWebhook(ctx)

    alt First Poll
        Handler->>Google: Seed cursor / timestamp
        Handler-->>Poll: success (emit nothing)
    else Subsequent Polls
        Handler->>Google: Fetch changes since last cursor
        Google-->>Handler: changes[]

        loop For each change
            Handler->>Idem: executeWithIdempotency(key, op)
            alt Cache hit (success)
                Idem-->>Handler: stored result (no re-fire)
            else Failed key + retryFailures=true
                Idem->>Idem: deleteKey() then retry
                Idem->>Proc: processPolledWebhookEvent()
                Proc-->>Idem: result
                Idem-->>Handler: result
            else New key
                Idem->>Proc: processPolledWebhookEvent()
                Proc-->>Idem: result
                Idem-->>Handler: result
            end
        end

        alt All succeed
            Handler->>Handler: Advance cursor/timestamp
        else Any failure
            Handler->>Handler: Revert cursor/timestamp
        end
        Handler-->>Poll: success/failure
    end
Loading

Reviews (9): Last reviewed commit: "fix(polling): remove extraneous comment ..." | Re-trigger Greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

waleedlatif1 and others added 11 commits April 9, 2026 18:24
- 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>
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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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
…empotency 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
… 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)
- 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.)
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment on lines +260 to +278
const overLimit = allChanges.length >= maxFiles

if (!hasMore || overLimit || pages >= MAX_PAGES) {
if (hasMore && !overLimit) {
lastNextPageToken = data.nextPageToken as string
}
break
}

lastNextPageToken = data.nextPageToken as string
currentPageToken = data.nextPageToken as string
}

const slicingOccurs = allChanges.length > maxFiles
const resumeToken = slicingOccurs
? (lastNextPageToken ?? config.pageToken!)
: (newStartPageToken ?? lastNextPageToken ?? config.pageToken!)

return { changes: allChanges.slice(0, maxFiles), newStartPageToken: resumeToken }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Drive cursor stalls permanently when the first page is exactly full

When pageSize === maxFiles (default both = 50) and the API returns exactly 50 changes with hasMore = true, the inner guard if (hasMore && !overLimit) is false (overLimit wins), so lastNextPageToken is never assigned in the break block. After the loop: slicingOccurs = (50 > 50) = false, the resolution chain tries newStartPageToken ?? lastNextPageToken ?? old_cursor. Since this is not the final page, newStartPageToken is absent and lastNextPageToken is still undefined, so the old cursor is reused.

Every subsequent poll re-fetches those same 50 changes as idempotency cache hits, marks success, and writes back the old cursor. Changes beyond the first 50 are permanently invisible until the backlog drops below 50 organically.

The previous fix for the "overshoots" case (addressed via slicingOccurs) covers length > maxFiles but not length === maxFiles. The guard should capture data.nextPageToken as lastNextPageToken whenever hasMore is true at the break point — not just when !overLimit.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ef82ce6. Configure here.

const computedEventType = determineEventType(event)
if (eventTypeFilter && computedEventType !== eventTypeFilter) {
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calendar advances cursor past events on unprocessed pages

Medium Severity

latestUpdated is tracked for ALL events in the batch — including those skipped by eventTypeFilter — before the filter check. When the event with the highest updated timestamp is a filtered-out event, the cursor advances past it even if that timestamp is beyond the pagination boundary. If maxEvents caused truncation via allEvents.slice(0, maxEvents), unprocessed events from later pages sharing timestamps near the filtered event's updated could be skipped permanently.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ef82ce6. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant