Analysis Automation

Time trackers that find repeat work: 5 compared, plus a script

Window titles reveal the loops worth automating, and they are the private part. Where five trackers keep them, and a script that counts the loops.

The short read

On a made-up workday, app totals showed nothing unusual; counting window sequences found one loop repeated 53 times.

  • ActivityWatch and ManicTime's free version keep window titles on your computer. RescueTime sends them to its servers; Toggl syncs them unless switched off.

What you can do with this

Run a local tracker on your own machine for one week, export the JSON, and count which three-window sequences repeat. The top line is your first automation candidate.

A closed red leather diary with an orange pencil slid under its strap, on a white background.
Photo: Philippe Alès, CC BY-SA 4.0
In this article
  1. A tally finds habits, and a log finds the order they come in
  2. Where five trackers keep your window titles
  3. Local trackers keep the sensitive part on your machine
  4. Cloud trackers send titles to a server, with off switches
  5. The test: one made-up workday, two ways of counting
  6. Two details decide whether the count is right
  7. The script, and how to run it on your own export
  8. What this means for you
  9. How we made this

Automatic time trackers can show which tasks you repeat, but only if they record window titles, and titles are the private part. “Order 10422 from customer” is the clue that one task ran 50 times. It is also a customer’s order number. So the first choice is where that log lives: on your computer or on a vendor’s server.

On October 21, 2026, we read the documentation and pricing pages of five automatic trackers: ActivityWatch, ManicTime, Timing, RescueTime and Toggl Track’s Timeline. Then we wrote a 118-line Python script that reads an ActivityWatch export and counts repeated window sequences, and ran it on a workday we made up. App totals showed nothing unusual. The sequence count found the loop.

A tally finds habits, and a log finds the order they come in

Our piece on noticing what to automate argues for a one-week tally on paper, because people misjudge their own habits. A tracker adds what paper misses: the order of windows. A task worth automating rarely lives in one app. It is a path, such as mail, then spreadsheet, then a supplier’s website, walked many times a day.

All five tools below record which app or document is active, with timestamps. That is enough to rebuild the path. None of them, by default, tells you which path repeats.

The ActivityWatch data model shows why: each window event holds an app, a title, a start time and a duration in seconds. The sequence is in the data. Someone has to count it.

Where five trackers keep your window titles

Every row comes from the vendor’s own page, read on October 21, 2026. Where a page did not say something, the table says so.

ActivityWatch ManicTime Timing RescueTime Toggl Track Timeline
Runs on Windows, macOS, Linux, Android Windows, macOS, Linux, Android, iOS Mac only Desktop, web, iOS, Android Windows and macOS desktop apps
Records Active app and window title; browser tab title and URL with an extension “Every application, document, website, and meeting”, with timestamps Window title and document path or URL, once a second App name, website URL, window title, start and end time App names and browser window titles or tab names
Where titles live Your computer (SQLite) Your computer, no account, when used on one machine A folder on your Mac; a built-in sync service exists RescueTime’s servers On your computer and synced to Toggl’s servers, unless you turn sync off
Cost Free, open source Free version for one machine; paid plans add sync 30-day trial; Professional $10 a month 14-day trial; Solo $9 a month or $84 a year We could not tell from the pricing page which plans include Timeline
Export JSON, from the web UI or the API Not stated on the pages we read A web API on the Connect plan CSV and raw exports on the Timesheets plans Not stated on the pages we read

Sources by column: ActivityWatch’s repository, data directories and export docs; ManicTime’s page for individuals and database location; Timing’s FAQ and pricing; RescueTime’s privacy policy and pricing; Toggl’s Timeline article, local data article and pricing. Timing’s $10 comes from the price data embedded in its pricing page, which also lists Connect at $168 a year; the other prices did not appear in the page text.

Local trackers keep the sensitive part on your machine

ActivityWatch stores its SQLite database in a folder on your machine, for example C:\Users\<USER>\AppData\Local\activitywatch\aw-server-rust on Windows and ~/.local/share/activitywatch/aw-server-rust on Linux, per its directories page. Its README describes “safe storage of the data on the user’s local machine” and says “you control the data”. It is under the MPL-2.0 license, and the latest release on its releases page was v0.14.1, dated October 10, 2026.

ManicTime’s page for individuals says that on a single machine “no account is required” and “all data stays on your computer”; paid plans “add cloud sync, multi-device access, and team features.” The database is two SQLite files, ManicTimeCore.db and ManicTimeReports.db, in AppData\Local\Finkit\ManicTime.

Timing keeps its data in Library/Application Support/info.eurocomp.Timing2 on the Mac, according to its FAQ, which also says “Apps running in the background are not recorded.” The same FAQ confirms a built-in sync service. It did not say there whether sync is optional, so check before you turn on a plan that includes it.

The trade-off with local tools is that nobody else sees the data, including you on another computer. ActivityWatch’s official window watcher is “X11 only” on Linux; Wayland desktops need a separate watcher from its list.

Cloud trackers send titles to a server, with off switches

RescueTime’s privacy policy lists what it records for time analysis: “application name, web site URL, window title, start time of use, end time of use.” It also says it does not collect “keystrokes, form input, screenshots, window or page body content.” You can stop the titles under Account Settings, Data & Privacy, by turning off “Collect window and document titles,” per its help article. That protects privacy and also removes the detail that tells one task from another.

Toggl’s Timeline is off until you enable it in the desktop app. Toggl’s support article says “we store the data locally and sync the data to our servers too,” and advises against the feature if titles hold sensitive information. The Timeline article says sync can be turned off. Synced Timeline data stays on Toggl’s servers for 90 days, and other workspace users, admins included, cannot see it.

Our view: for finding repeat work, a local tracker is the better default. You need titles for this job, and a local tool keeps them off third-party servers.

The test: one made-up workday, two ways of counting

We could not record a real person’s week, and we did not want to invent one. So we wrote a generator that builds one workday in ActivityWatch’s export format. Every number in it is ours, not a measurement of anyone.

The made-up day runs from 08:00 to about 16:30 in UTC+2. It mixes inbox time, team chat, a bank’s payment page and an 80-minute report in a document editor.

Hidden inside, 53 times, is one loop: an order email (20 to 60 seconds), the orders spreadsheet (30 to 90), the supplier’s ordering page (40 to 120). The order emails carry different numbers. There is a 15-minute coffee break, a 40-minute lunch, and a 30-minute meeting during which the spreadsheet stayed open on screen with nobody at the desk.

The export format follows ActivityWatch’s Bucket model: a buckets object, each bucket with a type, a hostname and its events. The window watcher’s bucket type is currentwindow and the away detector’s is afkstatus, as in their source code (window, afk).

Guess which app the made-up worker spent most time in, by the totals. It is Mail, at 2.4 hours of 7.1 active hours, followed by the browser at 2.0. Nothing in that list says “order entry.” The loop is spread across three apps, so no single total stands out.

Counted as sequences, the same data gives this. It is the real output of the script below, run with Python 3.11.17:

Active time: 7.1 h in 210 window steps
  Mail             2.4 h    34%
  Browser          2.0 h    28%
  Docs             1.4 h    19%

Most repeated 3-step sequences (seen 3 or more times):
   53 times,   163 min, 1 day(s),  184 s each
        Mail | Order # from customer - Mail
        Spreadsheet | orders-#.xlsx - Spreadsheet
        Browser | Supplier portal - New order - Browser
   11 times,    58 min, 1 day(s),  318 s each
        Mail | Inbox (#) - Mail
        Mail | Order # from customer - Mail
        Spreadsheet | orders-#.xlsx - Spreadsheet

The script found all 53 loops the generator made, worth 163 minutes, about 3 minutes each. The second line is mostly the same loop with the inbox in front of it. Read the top line first; the lines below it often share steps with it.

Two details decide whether the count is right

Numbers in titles hide the repeat. The order emails carry their own numbers, so without a fix almost every loop looks unique. The script replaces every run of digits with #, so “Order 10422” and “Order 10533” become one step. The cost: two windows that differ only by a number merge too.

Idle time inflates whatever window was open. We ran the script again on the same day without the away bucket. Active time rose from 7.1 to 7.6 hours, and the spreadsheet went from 0.9 to 1.4 hours: the 30-minute meeting counted as spreadsheet work. ActivityWatch’s away watcher uses a 180-second default timeout, so it marks you away after three minutes without keyboard or mouse input. Export both buckets.

Raw sequence counts also double up. A loop of A, B, C seen as B, C, A from a different starting point is the same loop. The script counts all rotations as one, and never counts two overlapping runs of the same loop.

The script, and how to run it on your own export

It uses only Python’s standard library and needs Python 3.11 or later. It reads the file and writes nothing.

#!/usr/bin/env python3
"""Find repeated window sequences in an ActivityWatch export. Reads only; writes nothing.

Usage: python3 aw_repeats.py export.json [--tz Europe/Stockholm] [--steps 3] [--top 10]
Needs Python 3.11 or later. On Windows, run "pip install tzdata" once if --tz fails.
"""
import argparse, json, re, sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

if sys.version_info < (3, 11):
    sys.exit("Needs Python 3.11 or later (older versions misread ActivityWatch timestamps).")

p = argparse.ArgumentParser()
p.add_argument("export")
p.add_argument("--tz", default="UTC", help="your time zone, for the day split (ActivityWatch stores UTC)")
p.add_argument("--steps", type=int, default=3, help="length of the sequence to count")
p.add_argument("--top", type=int, default=10)
a = p.parse_args()
try:
    tz = ZoneInfo(a.tz)
except ZoneInfoNotFoundError:
    sys.exit(f"Unknown time zone {a.tz!r}. On Windows, run: pip install tzdata")

with open(a.export, encoding="utf-8") as f:
    buckets = json.load(f).get("buckets", {})

def events(kind):
    """(host, start, end, data) for every event in buckets of this type, sorted by host, then time."""
    out = []
    for b in buckets.values():
        if b.get("type") == kind:
            for e in b.get("events") or []:
                start = datetime.fromisoformat(e["timestamp"])
                end = start + timedelta(seconds=float(e.get("duration", 0)))
                out.append((b.get("hostname", "?"), start, end, e.get("data", {})))
    return sorted(out, key=lambda x: (x[0], x[1]))

windows = events("currentwindow")
if not windows:
    sys.exit("No window events found. Export all buckets, not just the AFK one.")
away = defaultdict(list)                                     # afk stretches per computer
for host, s, e, d in events("afkstatus"):
    if d.get("status") == "afk":
        away[host].append((s, e))

def active_parts(host, s, e):
    """Cut the stretches marked afk on the same computer out of one window event."""
    parts = [(s, e)]
    for a_s, a_e in away[host]:
        nxt = []
        for p_s, p_e in parts:
            if a_e <= p_s or a_s >= p_e:
                nxt.append((p_s, p_e))
                continue
            if p_s < a_s: nxt.append((p_s, a_s))
            if a_e < p_e: nxt.append((a_e, p_e))
        parts = nxt
    return parts

def label(d):
    title = re.sub(r"\d+", "#", d.get("title", ""))    # "Order 10422" and "Order 10533" count as one step
    return f"{d.get('app', '?')} | {title}"

steps, by_app, last_host = [], Counter(), None
for host, s, e, d in windows:
    if host != last_host and steps:
        steps.append([s, None, 0, s])                        # a sequence never spans two computers
    last_host = host
    for p_s, p_e in active_parts(host, s, e):
        secs = (p_e - p_s).total_seconds()
        if secs < 1:
            continue
        by_app[d.get("app", "?")] += secs
        lab = label(d)
        if steps and steps[-1][1] == lab and (p_s - steps[-1][3]).total_seconds() < 5:
            steps[-1][2] += secs; steps[-1][3] = p_e            # same window again: one step
        else:
            steps.append([p_s, lab, secs, p_e])

total = sum(by_app.values())
if not total:
    sys.exit("All window time falls inside afk stretches; nothing to count.")
print(f"Active time: {total/3600:.1f} h in {sum(1 for x in steps if x[1])} window steps")
for app, secs in by_app.most_common(a.top):
    print(f"  {app:<14} {secs/3600:5.1f} h  {100*secs/total:4.0f}%")

def canonical(labs):
    """A>B>C, B>C>A and C>A>B are one loop seen from different starts: count it once."""
    return min(labs[i:] + labs[:i] for i in range(len(labs)))

seq_count, seq_secs, seq_days, last_end, shown = Counter(), Counter(), defaultdict(set), {}, {}
for i in range(len(steps) - a.steps + 1):
    chunk = steps[i:i + a.steps]
    labs = tuple(c[1] for c in chunk)
    if None in labs or len(set(labs)) < a.steps:
        continue                                           # skip A, B, A bouncing
    key = canonical(labs)
    if last_end.get(key, -1) > i:
        continue                                           # overlaps the last counted run of this loop
    last_end[key] = i + a.steps
    shown.setdefault(key, labs)                            # print the order first seen
    labs = key
    seq_count[labs] += 1
    seq_secs[labs] += sum(c[2] for c in chunk)
    seq_days[labs].add(chunk[0][0].astimezone(tz).date())

print(f"\nMost repeated {a.steps}-step sequences (seen 3 or more times):")
if not seq_count or seq_count.most_common(1)[0][1] < 3:
    print("  none yet; collect a few more days of data")
for labs, n in seq_count.most_common(a.top):
    if n < 3:
        break
    print(f"  {n:3d} times, {seq_secs[labs]/60:5.0f} min, {len(seq_days[labs])} day(s), "
          f"{seq_secs[labs]/n:4.0f} s each")
    for lab in shown[labs]:
        print(f"        {lab}")

To run it on your own data:

  1. Install ActivityWatch from its releases page and let it run for a normal working week. Add the browser extension if your repeat work happens in web apps; it records the active tab’s title and URL.
  2. Export everything. In the web UI, open Raw Data at http://localhost:5600/#/buckets and click “Export all buckets as JSON”, or fetch http://localhost:5600/api/0/export, per the export docs.
  3. Run python3 aw_repeats.py export.json --tz Europe/Stockholm with your own time zone. ActivityWatch stores timestamps as UTC; the time zone only decides which day a loop counts toward.
  4. Try --steps 2 for copy-and-paste between two windows, and --steps 4 for longer paths.
  5. Price the top line. Multiply the minutes a day by working days, and take off upkeep with our payback formula.

What this means for you

The choice is between privacy and convenience, and for this job privacy is cheap. If you want to find your own repeat work, run ActivityWatch or ManicTime’s free version on your own machine for a week, export, count, then delete what you no longer need. Choose Timing if you work only on a Mac, after you check how its sync works. Choose RescueTime or Toggl only if you need the cloud dashboard, and know that the titles go with it unless you switch them off.

Do not use any of these to watch other people without doing the legal work first. A tracker on a colleague’s computer is employee monitoring, and our piece on employee monitoring and GDPR explains why consent usually fails and what a small company must write down. ActivityWatch’s own export docs advise aggregating and redacting data locally before giving it to an AI assistant. The script’s output still contains titles, so read it before you share it.

If the top loop is mail to spreadsheet, start with our guide to getting email data into Excel. If it is something stranger, check it against the xkcd table with upkeep counted before you build anything.

How we made this

On October 21, 2026, we read the documentation and pricing pages of the five trackers above, plus their privacy pages where they had one, and the ActivityWatch watcher source code on GitHub. We wrote the script and a generator for one made-up workday in ActivityWatch’s export format, and ran both with Python 3.11.17; the output above is real, the workday is not. We tried to record a live session with ActivityWatch’s watchers on a virtual Linux desktop, but the older Python server we installed from PyPI (0.12.0) failed on its first requests, so no live recording is in this piece, and we did not install the other four trackers.

Sources