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.tssable.toggl.hours schedule
  • supabase/migrations/0007_toggl_hours.sql — additive hours columns
  • supabase/migrations/20260903170000_descope_toggl_drive_from_report_readiness.sql — checkpoint scoping