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_loggedhours_30dhours_synced_attoggl_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
| Setting | Value |
|---|---|
| Secret | TOGGL_API_KEY |
| Write scope | Hard-limited to projects.hours_logged, hours_30d, hours_synced_at, toggl_project_id — nothing else, ever |
| Checkpoint requirement | Explicitly 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
| Task | Schedule | Params |
|---|---|---|
sable.toggl.hours | Every 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
| Table | Written by | Contents |
|---|---|---|
projects.hours_logged / hours_30d / hours_synced_at / toggl_project_id | Hours mode only | Additive telemetry; never the project name, client, code, or status |
activity_events | Hours mode | Daily 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_checkpoints | Both modes | Health/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_checkpointsshows Toggl as required/blocking something it shouldn't. Confirm therequired:falseoverride is still present in the migration/data (see20260903170000_descope_toggl_drive_from_report_readiness.sql) — the checkpoint helper's default isrequired:true, so a code change that drops the explicit override silently re-adds Toggl to the readiness gate.healthshowsfailedfor 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 onactivity_events; the fix writes closed-day totals only, once.
Where the code lives
supabase/functions/toggl-sync/index.ts— drift report and hours refreshtrigger/connectors.ts—sable.toggl.hoursschedulesupabase/migrations/0007_toggl_hours.sql— additive hours columnssupabase/migrations/20260903170000_descope_toggl_drive_from_report_readiness.sql— checkpoint scoping