All posts
How it worksTask trackingAutomatic timesheetsOn-device matchingProduct engineering

How tracked time finds the right task

Automatic timesheets usually work by guessing, and a wrong booking costs more than a missing one. Here is the matcher we shipped instead, thresholds and all.

TO
The TimerOS Team
Vezoft
8 min read

Everyone who has ever kept a timesheet knows the Friday afternoon ritual: staring at a week you definitely worked, trying to remember which of it was the migration and which was the bug that ate Tuesday. The hours were real. The record of which task they belonged to was never written down, because writing it down meant stopping work to press a button.

TimerOS has tracked the hours automatically since launch. What it could not do until this month was say what they were for. That shipped across v1.3 — the desktop app now matches tracked time to your own open tasks while you work, on your machine. This post is about how it decides, and more usefully, about when it decides to say nothing at all.

It is in beta, and it runs on Windows
Task matching is marked Beta in the desktop app while it settles. It is on by default, you can switch it off in Settings, and every automatic booking can be reviewed, reassigned or unassigned. It runs in the Windows desktop app only — the web dashboard has no matcher — and it does not touch invoicing: your hours fill in, but an invoice is still assembled by hand.

Why guessing is the failure mode

The obvious way to build this is to take whatever the person is looking at, find the closest-sounding task, and book the time. It demos beautifully and it is quietly corrosive, because a timesheet that is 80% right is worse than one that is 60% filled. The 20% is invisible — it looks exactly like the correct rows — so you either audit everything, which costs more than filling it in did, or you bill a client for work that happened on a different job.

A wrong booking is more expensive than a missing one, because you cannot see it.
This is the design constraint the whole matcher is built around.

So the matcher is built to abstain. It has to clear a real evidence bar before it books anything, and when it cannot, the time simply stays productive and unattributed. Unassigned time is a normal outcome here, not a failure state, and the app does not nag you about it.

What it actually reads

One signal: the title of the window you are looking at. Not the application name, not the browser address. Those two feed the separate productive-versus-idle classification and carry no weight in matching a task — the host parameter is passed into the scoring function and deliberately ignored.

That title is tokenised and scored against a corpus built from your own open tasks and subtasks. Words from a task’s title count at full weight; words from its description count at 40%, and can only ever raise a score, never dilute it. Rarer words count for more than common ones, and about 130 generic words — fix, bug, update, task — are stripped out entirely, so “Fix login bug” and “Fix export bug” are separated by the words that actually differ.

The evidence bar

Before anything is booked, a candidate has to pass all of these:

  • Two matching content words — or one that is unique. A single shared generic word can never carry a match. One word will do it only if that word appears in exactly one task in your whole list, which is what makes ticket numbers work.
  • A score of 0.45, measured as weighted coverage of the task’s own title words.
  • A winning margin of 0.15 over the runner-up. Two similar tasks that score closely produce no booking at all, rather than a coin flip. This is the rule that does the most work.
  • Sixty seconds of agreement. The same task has to keep winning across repeated checks spanning a full minute before its first booking. Alt-tabbing through something for fifteen seconds books nothing.
Figure 1 · The gates a match has to pass
0
matching words, or 1 unique
0.00
minimum score
0.00
margin over runner-up
0s
before the first booking
Read from attribution.rs. A candidate failing any one of these produces no booking — the time stays productive and unattributed.

Two thumbs on the scale

Coverage alone ignores everything you already know about the work, so two priors adjust it. A task marked in progress is weighted up by a quarter. And a task’s dates matter: work happening inside its scheduled window gets a modest lift, while a task that is more than a week from starting, or a fortnight past due, is weighted down to 60%. The date is taken from the same trusted clock the shift logic uses, so changing your machine’s timezone does not move it.

Staying still, and letting go

The other half of the problem is not accuracy, it is stability. A matcher that re-decides every five seconds produces a day chopped into ninety fragments, which is technically detailed and practically useless.

So a match latches. Once a task holds, a different task has to out-score it continuously for two full minutes before the booking switches — a quick glance at another ticket does not derail the block you are in. And a latch that has gone thirty minutes without fresh confirmation lets go by itself, so a task you finished at eleven does not keep quietly collecting your afternoon.

Figure 2 · The timing rules, in seconds
How long a task must holds
20
60
120
1800
In-app dwellFirst bookingSwitch awayLatch decay
Entry is deliberately shorter than switching: it should be easy to start attributing, and harder to be pulled away.

There is a second, quieter channel. When you are inside TimerOS itself with a task, incident or change request open on screen for twenty seconds, that counts as context directly — no guessing required, because you are literally looking at the thing. It is the only route by which incidents and change requests get attributed at all.

What leaves your machine

All of the above happens in the desktop app, in Rust, on your own computer. The corpus is built locally from your assigned work, the scoring is local, the latch is local.

What syncs is a map of task IDs to second counts — the same shape as the rest of the day’s hours. Window titles are not transmitted, because the thing that needed to read them already finished reading them before anything left the machine. That is the same architecture as the classification itself, which is written up separately.

Worth saying plainly: none of this is AI. There is no model in the matching path, no inference server, nothing that needs a GPU. It is a deterministic scoring function — the same input produces the same output every time, and it behaves identically whether your tracking mode is Automatic or Rule-based. That is not modesty. Determinism is why we can tell you the exact thresholds in this post, and why you can predict what it will do.

How this differs from auto-tracking elsewhere

Background capture is not new, and we should not pretend it is. Both Toggl Track and Clockify record activity without a timer running. The difference is what arrives at the other end.

Figure 3 · Two ways to end up with a filled timesheet
Record-and-reviewMatch-on-device
What is capturedA list of the apps and sites you usedA decision about which task the time belongs to
Who does the sortingYou, afterwards — converting entries by handThe matcher, while you work
When it is unsureEverything is kept; you decide what countsNothing is booked; the time stays productive
What you do at the endBuild the timesheet from the recordCheck the exceptions, fix what is wrong
Checked against each vendor's own documentation on 4 August 2026. Both can also start a timer automatically from a rule you write in advance — Toggl's can even pre-set the project — but the rule is authored by you, not inferred.

Neither approach is obviously right. Record-and-review never books anything wrong, because it never books anything — the cost lands on you on Friday. Matching moves that cost to a machine and accepts that it will sometimes be wrong, which is why the review panel and the undo matter more than the matcher does.

What we deliberately did not do

  • No project-level fallback. The first version booked to the project when it could not identify a task. We removed that five hours later, in v1.3.1. “Some time, on roughly the right project” is exactly the plausible-looking noise that makes a timesheet untrustworthy.
  • No nagging. There is an Unassigned row showing time that matched nothing, with a one-click assign if you want it. It does not turn red, and it does not follow you around.
  • No silent overwriting. Tracked hours appear on a task on their own, but a number you typed always wins over a number that was derived. The task shows which it is.
  • No route to your invoice. Attributed hours do not flow into billing yet. Given that a wrong booking would become a wrong invoice, that ordering is deliberate — the matcher earns billing later, once beta has taught us where it is weak.
Where to find it
Update the Windows desktop app to v1.3.4. Matching is on by default; the timer bar shows what it thinks you are working on, and the Task time panel on the Performance page is where you review, reassign or unassign anything. It is included on every plan, Freelancer up, with no tier gate. Download TimerOS for Windows or start a free trial.
Tell us where it is wrong
Beta is not a disclaimer here, it is a request. The thresholds in this post are our first honest guess at the trade between booking too eagerly and abstaining too often, and the only way to tune them is against real task names. If it is booking time to the wrong task, or sitting out days it should have caught, we want the example — [email protected].

The goal was never a timesheet that fills itself in perfectly. It was to move the work from remembering to checking — because you can check a list in two minutes on Friday, and you were never going to remember Tuesday.

See it on your own machine.

Fourteen-day trial, no card needed up front. Install the desktop app and watch a day classify itself — no screenshots involved.