Skip to main content
← Back to work

Automated anomaly triage for 100+ specialists' timesheets

PMs were rubber-stamping hundreds of timesheet lines a month. And the wrong ones (weekend hours, days over eight, time booked on approved leave) were exactly the ones that slipped through, because nothing surfaced them and the tool timed out before the data even loaded.

JavaScriptVanilla JSZohoZoho Sigma SDKZohoZoho Projects APIZohoZoho People APIZohoZoho Analytics APIPythonPythonFlaskFlaskGoogle CloudGoogle Cloud RunGoogle Cloud StorageGoogle Cloud StorageThreadPoolExecutor

How it fits together

01 Approval widget anomaly-triage UX
02 Flask cache-through Google Cloud Run
03 GCS cache per-resource TTL
04 Zoho APIs Projects, People, Analytics
Core idea

Most requests never touch Zoho. A warm cache serves reads; only writes go through a server-side token.

  • Nightly warm-up keeps the cache fresh
  • Three-client OAuth round-robin spreads Zoho rate limits
timesheet triage / architecture

The consultancy bills clients from time specialists log in Zoho Projects, and PMs must approve it monthly before invoicing. The native approval screen is a flat, unranked list with no signal about which entries are suspicious. So PMs either approved blindly or spent hours eyeballing calendars across dozens of projects. And real exposure is cross-project: a specialist can blow past a sane daily total only because of hours on another PM’s project, which a single-scope review never shows.

Two problems. Approval is a signal-detection task, not a list-scrolling task. Anomalies need to be ranked and explained, and the clean majority cleared in one action. And calling Zoho directly per project and user doesn’t scale: a cold session for a large portfolio made on the order of 272 Zoho API calls, hammering Zoho’s rate limits and leaving the widget slow. And a rate-limited call (Zoho error 6045) can surface as a failure mid-load. The stakes are billing: if a load quietly comes back short, invoiceable hours go un-reviewed.

  • Built a weighted anomaly-scoring engine: each specialist’s days are scored by time logged on declared leave (5.0 points/hour), weekend work (3.0), daily overtime beyond 8h (2.0), and weekly volume beyond 50h (1.5), summed into a score and bucketed into severity bands at 5 / 20 / 50 (none / low / medium / high). Each flag carrying a plain-language ‘why’.
  • Reframed the UI as triage: flagged specialists appear one at a time in a severity-ordered carousel, while everyone clean collapses into a single bulk-approve action.
  • Folded a specialist’s out-of-scope hours into their totals with a background cross-project scan, so portfolio-wide overtime surfaces even though a PM can only approve their own projects.
  • Built a GCS cache-through backend (Flask on Cloud Run) with per-resource TTLs. Profile photos 24h, project list and PM-scope 6h, timelog reports and specialist detail 4h. So most requests never touch Zoho.
  • Warmed the cache nightly for the right month (previous month before the 15th, current month from the 15th), so a PM lands on a warm cache instead of triggering the fetches.
  • Chunked project reads (30 IDs per cached request), ran fetches through a 5-wide thread pool, and pushed approvals in batches of 50 through a server-side OAuth token. The widget’s ambient session can’t authorize writes.
  • Cut Zoho API traffic ~98%. A cold session for a large portfolio dropped from ~272 calls to ~5 with the cache warm. Via a cache-through over Google Cloud Storage, using GCS’s strong read-after-write consistency so no separate lock was needed.
  • Spread Zoho’s rate limits across a three-client OAuth round-robin, parking any client that hits a limit (Zoho error 6045) for an hour and refreshing tokens 30s before expiry.
  • Routed every approval through the backend’s server-side OAuth token, because the widget’s ambient session is rejected on writes. A non-obvious correctness requirement that isn’t visible until writes start failing.
Zoho API calls per session 98% fewer
Before
~272 (cold)
After
~5 warm · ~98% less
Timesheet review
Flat unranked list, blind approval Severity-ranked triage; bulk-approve the clean cohort, drill down only on flagged specialists
Cross-project overtime
Invisible in a single-project view Surfaced by a background cross-project scan

My role

Sole engineer. The anomaly-scoring engine, the triage UX, and the Flask/GCS caching backend.

Stack & scope

Monthly timesheet approval across a portfolio of hundreds of client projects and dozens of PMs and specialists.

Client and internal identifiers changed to preserve confidentiality.