connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

connector-airtable: webhook-first sync, drop standard-plan comments - #129

Merged
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments
Apr 30, 2026
Merged

connector-airtable: webhook-first sync, drop standard-plan comments#129
KrisBraun merged 1 commit into
mainfrom
airtable/drop-comments

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Summary

Replaces the 30-minute "walk every record in every detected task table" reconcile loop with a webhook-first architecture that uses Airtable's delivery + cursor model as the truth source, plus explicit health tracking and auto-recovery for the failure modes the old polling masked.

The 30-min reconcile was masking three real reliability problems:

  • Webhooks expire 7 days after creation/refresh; the connector never refreshed them, so they silently died and the reconcile became the only thing keeping data flowing.
  • Missed deliveries (transient 5xx, network blip) advanced no cursor and were never retried, dropping changes until the next reconcile re-read every record.
  • Schema changes (renamed status field, new options) weren't subscribed to, so cached task_tables_${baseId} went stale until the next channel re-enable.

Comment sync is dropped entirely. Airtable's standard webhooks have no comment data type — that's only available via the Enterprise-only Change Events API — and adding a comment doesn't update Last Modified Time either, so there's no efficient filterByFormula workaround. Removing it eliminates the dominant per-reconcile cost (one listComments call per record).

What's new

  • Webhook expiration trackingsetupWebhook persists expirationTime and schedules a renewWebhook ~24h before expiry. On /refresh it persists the new expiration and re-schedules; on 404 it recreates from scratch; on auth failure it bails (user reconnect required); on transient errors it retries in 1h.
  • Schema-change subscriptiondataTypes is now ["tableData", "tableFields"]; recordChangeScope dropped so the subscription is base-wide. applyPayload re-runs scoreTable when field events arrive, keeping the task_tables map fresh without webhook recreation.
  • Self-rescheduling payload drainprocessWebhookPayloads self-reschedules when it exits the 10-batch guard with mightHaveMore: true; bumps last_activity_at_${baseId} on ingestion.
  • 6h safety poll — new pollWebhookPayloads runs every 6h per enabled channel: catches missed deliveries via cursor, keeps the webhook alive (the /payloads endpoint resets Airtable's 7-day inactivity timer), and triggers recoverWebhook on a 404, missing webhook id, or 14+ days of total silence.
  • Auto-recoveryrecoverWebhook clears webhook state, bumps last_activity_at to "now" (so a failed recovery doesn't re-fire next poll), then queues setupWebhook + detectAndSync(initialSync=false) to re-emit current record state after a confirmed-or-suspected gap.
  • Comment sync removed — drops data.recordComments:read/:write scopes, handleReplies, onNoteCreated/onNoteUpdated, mention translation helpers, and the listComments/createComment/updateComment client methods.

Migration

In-flight reconcileComments tasks from currently-deployed connectors keep dispatching after upgrade — the method is preserved as a one-shot shim that runs:

  1. Phase 2 backfill: schedules renewWebhook for channels missing webhook_expires_at (would otherwise silently die at the 7-day mark)
  2. Phase 4 backfill: bootstraps pollWebhookPayloads, gated by a poll_initialized flag also set in onChannelEnabled to prevent duplicate poll chains

Then exits without rescheduling. After ~1 hour post-deploy, all in-flight reconcile tasks have fired once and the shim can be deleted in a follow-up.

Existing comment-keyed Plot notes (key="comment-<airtableId>") remain in users' threads as orphan local notes — no data deletion, just no further sync to/from Airtable.

Cost reduction

Old: every base, every 30 minutes, ~2N requests for N records (full table walk + per-record listComments).

New:

  • Real-time record updates via webhook delivery
  • 6-hourly safety poll: ~1 request unless deliveries were missed
  • 14-day stale recovery for quiet bases: ~N requests per 14 days (one detectAndSync)

For a 1k-record base: ~700× cost reduction.

Test plan

  • Connect a fresh Airtable base, verify webhook is created with dataTypes: ["tableData", "tableFields"] (check Airtable's webhook list endpoint or stored webhook id)
  • Make a record edit in Airtable, verify Plot thread updates within seconds
  • Rename a status field's options, verify the cached task_tables_${baseId} reflects the new option names without channel re-enable
  • Disable channel, verify all *_${baseId} storage keys are cleared
  • Re-enable channel, verify fresh webhook + initial detectAndSync runs
  • (Time-dependent) Verify renewWebhook runs ~6 days after channel enable and pushes expiration out
  • (Time-dependent) Verify pollWebhookPayloads runs every 6h and processes any pending payloads
  • Manually delete the webhook in Airtable; on next safety poll, verify connector recreates it and triggers detectAndSync
  • Verify pre-deploy in-flight reconcileComments tasks fire once after upgrade and don't reschedule (check via storage keys + log absence after 30 min)

🤖 Generated with Claude Code

Replaces the 30-minute "walk every record in every detected task table"
reconcile loop with a webhook-first architecture that uses Airtable's
delivery + cursor model as the truth source and adds explicit health
tracking + auto-recovery for the failure modes the old polling masked.
The 30-min reconcile was masking three real reliability problems:
- Webhooks expire 7 days after creation/refresh; the connector never
refreshed them, so they silently died and the reconcile became the
only thing keeping data flowing.
- Webhook payload deliveries that failed (transient 5xx, network
blip) advanced no cursor and were never retried, dropping changes
until the next reconcile re-read every record.
- Schema changes (renamed status field, new options) weren't
subscribed to, so cached `task_tables_${baseId}` went stale until
the next channel re-enable.
Comment sync is dropped entirely. Airtable's standard webhooks have no
comment data type — that's only available via the Enterprise-only
Change Events API — and adding a comment doesn't update Last Modified
Time, so there's no efficient `filterByFormula` workaround either.
Removing it eliminates the dominant per-reconcile cost (one
listComments call per record) and simplifies the runtime.
What's new:
- setupWebhook persists `expirationTime` and schedules a renewWebhook
task ~24h before expiry. renewWebhook calls /refresh, persists the
new expiration, and re-schedules. On 404 it recreates from scratch.
- dataTypes is now ["tableData", "tableFields"]; recordChangeScope
dropped so the subscription is base-wide. applyPayload re-runs
scoreTable when field events arrive, keeping task_tables fresh
without webhook recreation.
- processWebhookPayloads is now public, self-reschedules when it
exits the 10-batch guard with mightHaveMore still true, and
bumps last_activity_at_${baseId} on ingestion.
- New pollWebhookPayloads runs every 6h per enabled channel as a
safety net: catches missed deliveries via cursor, keeps the
webhook alive (the /payloads endpoint resets Airtable's 7-day
inactivity timer), and triggers recoverWebhook on a 404, missing
webhook id, or 14+ days of total silence.
- recoverWebhook clears webhook state and queues setupWebhook +
detectAndSync(initialSync=false) to re-emit current record state
after a confirmed-or-suspected gap.
- Drops `data.recordComments:read`/`:write` scopes, `handleReplies`,
`onNoteCreated`/`onNoteUpdated`, mention translation, and the
listComments/createComment/updateComment client methods.
Migration: in-flight reconcileComments tasks from currently-deployed
connectors keep dispatching after upgrade — the method is preserved as
a one-shot shim that runs Phase 2 backfill (renewWebhook for channels
missing webhook_expires_at) and Phase 4 backfill (pollWebhookPayloads
bootstrap, gated by a poll_initialized flag also set in
onChannelEnabled to prevent duplicate poll chains), then exits without
rescheduling. The shim is safe to delete in a follow-up after ~1h.
Existing comment-keyed Plot notes (key="comment-<airtableId>") remain
in users' threads as orphan local notes — no data deletion, just no
further sync to/from Airtable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit dce62f8 into mainApr 30, 2026
1 check passed
@KrisBraun
KrisBraun deleted the airtable/drop-comments branch April 30, 2026 02:10
Sign up for freeto 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

@KrisBraun