Skip to main content

Connector Tasks

Overview

trigger/connectors.ts owns every scheduled sync that pulls external data into SABLE's Supabase tables: Fathom meeting notes, Granola notes, Asana tasks, Toggl hours, Google Calendar free/busy, Google Drive documents, and inbound email. Each sync is a thin Trigger.dev wrapper around one SABLE Supabase edge function.

trigger/approvals.ts's executeApproval is grouped here too, even though it lives in a different file: it is the write-back half of the same connector surface. Where the tasks below pull data in, executeApproval pushes a human-approved action out to Asana or Google Calendar (asana-act / calendar-act), so it belongs next to the read-side connectors rather than in any of the other three pages.

How it works

Every connector task funnels through runConnector(), a shared helper that:

  1. Builds a payload carrying a trigger envelope (task_id, scheduled_at, schedule_id).
  2. Opens a trigger_runs row via createTriggerRun before doing any work, so every invocation is auditable even if it later fails.
  3. Calls the target Supabase edge function through invokeSableEdge (trigger/lib/edge.ts), which wraps the HTTP call in retry.fetch with timeout.maxAttempts: 5 and connectionError.maxAttempts: 5.
  4. Marks the trigger_runs row completed or failed via completeTriggerRun / failTriggerRun.

Three connectors — Fathom, email, and (via a separate always-on task) Drive — don't stop after one edge-function call. Fathom, Granola-style backlogs can outrun a single 110-second edge-function budget, so fathomReconcile and emailDrain loop, re-invoking the same edge function while its response says stopped_early: true, up to a bounded pass count and a wall-clock run budget. driveRefreshContinuation does the equivalent by chaining a new task run (.trigger(..., { delay: "30s" })) rather than looping in place. All three treat "some work failed, but the run made net progress" as a warning to log, not a reason to fail the whole run — only a run with zero cumulative successes is fatal. This distinction (see driveBatchOutcome in trigger/lib/drive-outcome.ts, re-exported from connectors.ts) exists because a single failing file inside a batch of one (batchSize: 1) always looks like a 100%-failed call in isolation, before the edge function's own retry-then-skip budget gets a chance to run.

Two connectors carry an unusual, undocumented-elsewhere requirement in their payload: asanaSync must pass workspaceGid (the Asana PAT can see more than one workspace and the edge function refuses to guess without it) and togglHours must pass hours: true (without it toggl-sync runs its drift check and writes no hours at all). Both traps are called out in code comments because a payload that looks complete, and a run that reports success, can still silently skip the actual work.

Tasks

TaskTrigger / cronWhat it does
fathomReconcileschedule, cron 15 */4 * * *Drains the Fathom meeting backlog. Loops up to 8 passes (FATHOM_MAX_PASSES) inside a ~25-minute budget, calling fathom-sync with limit: 25, lookbackDays: 3 each pass. Logs (does not fail) when it is still behind after all passes.
granolaPollschedule, cron 5,35 * * * *Polls Granola for new notes, limit: 25, lookbackHours: 72, single call to granola-sync.
asanaSyncschedule, cron 25 */2 * * *Refreshes Asana tasks with confirm: true, limit: 50, scoped to ASANA_WORKSPACE_GID. Runs independently of board generation so Asana no longer goes stale on weekends.
togglHoursschedule, cron 55 */4 * * *Refreshes Toggl hours (hours: true) so account/project screens and chat answers about burn stay current even though the board itself no longer cites Toggl.
calendarFreebusyschedule, cron 40 * * * *Refreshes calendar free/busy and events, days: 14, via calendar-sync.
driveRefresh / driveRefreshContinuationno cron — plain task, invoked by hand or by the continuation chainBatched Drive folder scan and ingest (batchSize: 1, up to 40 batches per invocation). Currently dormant by design: Drive ingestion is off (project folders are link-only), so drive-sync returns from its paused branch immediately. Kept invokable because credentials and retry logic are tested and the decision may reverse. driveRefreshContinuation is the exact same run function, exposed as a distinct task id so it can be scheduled/queued separately from a manual driveRefresh call.
emailIngestschedule, cron */10 * * * *Ingests new inbound email, limit: 200, maxNew: 5, single attempt (retry: { maxAttempts: 1 }) — the schedule itself is the retry, deliberately, after a 546 edge-worker-ceiling incident on 2026-09-09 where task-level retries tripled a pile-up (25 invocations in 23 minutes).
emailDrainschedule, cron 35 */2 * * *Bounded drain of whatever emailIngest's ten-minute cap can't clear: up to 6 passes (EMAIL_MAX_PASSES), same stopped_early loop pattern as Fathom.
executeApproval (approvals.ts)event — triggered when the app dispatches an approved actionClaims an approvals row, stamps it with the Trigger run id, then calls asana-act or calendar-act (selected by payload.edgeFunction) to actually perform the approved write. Runs on connectorWriteQueue.

What can go wrong / how to investigate

  • A connector looks "stale" on the board even though it's running. Check whether it's mid-drain: fathomReconcile and emailDrain intentionally report partial coverage and pick up where they left off next cron tick. Look at the task's still_behind / fathom_still_behind output and the fathom_passes / email_passes metadata rather than assuming a failure.
  • Asana sync 500s on every run. Almost always a missing or stale ASANA_WORKSPACE_GID env var — the edge function refuses to guess which workspace to scope to.
  • Toggl hours "succeed" but nothing changes on the account screen. Confirm the payload actually carries hours: true; a valid-looking payload without it runs the drift check only.
  • A batch of Drive or email work looks 100% failed. Check driveBatchOutcome's fatal-vs-partial distinction before treating a single failing file as an outage — a poison file exhausting its own retry budget inside the edge function is expected and is logged, not thrown, as long as the run's cumulative totals include at least one success.
  • sable.email.ingest runs are piling up. This connector deliberately has no task-level retry; if you see repeated failures, look at the edge function's own health (worker resource ceiling / 546s) rather than Trigger.dev retry configuration — the fix here was removing retries, not adding them.
  • An approval never executes. Check the approvals table row's trigger_run_id (set by executeApproval right after it claims the row) and the corresponding trigger_runs entry for the actual asana-act / calendar-act failure reason.

Where the code lives

  • sable-agents-demo/trigger/connectors.ts — all scheduled sync tasks.
  • sable-agents-demo/trigger/approvals.tsexecuteApproval (connector write-back).
  • sable-agents-demo/trigger/lib/edge.tsinvokeSableEdge, the shared Supabase edge-function caller with retry/timeout policy.
  • sable-agents-demo/trigger/lib/drive-outcome.tsdriveBatchOutcome fatal/partial classification.
  • sable-agents-demo/trigger/lib/env.tsrequireEnv (used for ASANA_WORKSPACE_GID).
  • sable-agents-demo/trigger/lib/supabase.tscreateTriggerRun / completeTriggerRun / failTriggerRun / serviceClient.
  • sable-agents-demo/trigger/queues.tsconnectorQueue (concurrencyLimit: 2) and connectorWriteQueue (concurrencyLimit: 1), shared across most tasks on this page.
  • Supabase: the trigger_runs table (migration 0041_trigger_runs.sql) is the audit trail for every connector invocation; the approvals table gained a trigger_run_id column in the same migration so an approval's execution can be traced back to its Trigger run.