Skip to main content

Toggl

Toggl is used for scope and pricing telemetry, not for owning any part of the project/client list. The connector is explicitly report-only for that master data — a hard-coded guard prevents it from ever writing project names, clients, codes, or status.

How it works​

toggl-sync authenticates with TOGGL_API_KEY (HTTP Basic, base64(key:api_token)) against the Toggl API v9 and Reports API v3. It runs in one of two modes:

Drift report (default)​

Compares Toggl workspaces/projects/clients against the SABLE projects/clients tables by normalized-name matching and reports what's in Toggl but not the spine, and vice versa, at both the project and client level. This mode never writes anything — ALLOW_WRITE is a hard-coded false in the function, independent of any request parameter.

Hours refresh ({ hours: true })​

Pulls time totals from the Toggl Reports API (/reports/api/v3 summary/time_entries), grouped by project and user, and writes only additive telemetry columns on projects:

  • hours_logged
  • hours_30d
  • hours_synced_at
  • toggl_project_id

Before writing, it runs a coverage guard (countDistinctTrackedUsers) that flags when the API token can only see one person's time entries — a documented prior failure mode where one person's Toggl hours were recorded as if they were the whole team's total. The guard doesn't silently correct this; it surfaces the condition so an operator can check the token's workspace access.

Matching prefers the already-stored projects.toggl_project_id; if a project has no stored Toggl id yet, it falls back to normalized name/client matching.

Configuration​

SettingValue
SecretTOGGL_API_KEY
Write scopeHard-limited to projects.hours_logged, hours_30d, hours_synced_at, toggl_project_id — nothing else, ever
Checkpoint requirementExplicitly required:false — Toggl was descoped from the connector-health readiness gate (2026-09-03); this must be stated at the call site because the shared checkpoint helper defaults required:true, and omitting it would silently re-scope Toggl back into the readiness gate

Modes​

{ "dryRun": true }
{ "dryRun": true, "hours": true }
{ "hours": true }

Cron cadence​

TaskScheduleParams
sable.toggl.hoursEvery 4 hours, :55 (55 */4 * * *){hours:true}

Toggl has no board-run requirement (removed 2026-08-05) — its schedule runs independently of any report/board generation cadence.

Table mapping​

TableWritten byContents
projects.hours_logged / hours_30d / hours_synced_at / toggl_project_idHours mode onlyAdditive telemetry; never the project name, client, code, or status
activity_eventsHours modeDaily closed-day totals only — activity_events is append-only, and rewriting an open day's running total would trip the append-only guard; a mismatch here caused health:"failed" from 2026-08-21 through 2026-09-09 until this was corrected
connector_checkpointsBoth modesHealth/error bookkeeping, marked required:false

projects and clients are also read (not written) for the drift comparison.

Failure reporting​

API failures during the drift or hours pull are reported by workspace and year (or the 30-day window for hours), rather than as one opaque error — so a partial telemetry failure for one workspace is visible without hiding data that did succeed elsewhere.

What can go wrong​

  • Hours look wrong for the whole team. Check the coverage-guard output first: if the API token can only see one person's time entries, the totals reported are that person's alone, not the team's. This was a real historical bug; the guard exists specifically to surface it rather than let it recur silently.
  • A project's name/client/status changed in Toggl but SABLE didn't follow. This is expected — Toggl is intentionally never allowed to touch the master project/client list, even in principle. Use the drift report to see the mismatch, and reconcile it manually in SABLE if needed.
  • connector_checkpoints shows Toggl as required/blocking something it shouldn't. Confirm the required:false override is still present in the migration/data (see 20260903170000_descope_toggl_drive_from_report_readiness.sql) — the checkpoint helper's default is required:true, so a code change that drops the explicit override silently re-adds Toggl to the readiness gate.
  • health shows failed for a stretch and hours still look right. This matches a known window (2026-08-21 to 2026-09-09) where writing an open day's running total tripped the append-only guard on activity_events; the fix writes closed-day totals only, once.

Where the code lives​

  • supabase/functions/toggl-sync/index.ts — drift report and hours refresh
  • trigger/connectors.ts — sable.toggl.hours schedule
  • supabase/migrations/0007_toggl_hours.sql — additive hours columns
  • supabase/migrations/20260903170000_descope_toggl_drive_from_report_readiness.sql — checkpoint scoping