A services business sells hours and invoices documents, and the distance between those two facts is where the money goes missing. The chain runs: someone does work, someone records it, someone decides it is billable, something prices it, someone approves it, and eventually a document goes to a client. Every link in that chain is a place where an hour that cost you salary stops being worth anything.
This is not a tooling complaint. It is a data-modelling one. The reason unbilled hours age out is almost always that the hour and the invoice live in different systems with no enforced relationship between them.
The six states a billable hour passes through
A billable hour is not a single fact. It is a record that moves through six states, and a services system is only as good as its handling of the transitions.
| State | The question it answers | What breaks here |
|---|---|---|
| Captured | Did the hour get recorded at all? | Logged from memory on Friday, attributed to the wrong project |
| Classified | Is this billable to the client? | Everything marked billable by default, then argued about later |
| Priced | At what rate? | The rate is looked up at invoice time, not at work time |
| Approved | Does a person stand behind this? | The approval queue is somebody’s inbox |
| Converted | Is it on an invoice? | Converted twice, or never |
| Posted | Is it in the books? | The invoice exists in a billing tool and not in the ledger |
Most firms have tooling for state one and state six, and improvise states two through five. Improvisation at state four is the expensive one.
Rate resolution is a specificity problem, not a lookup
The price of an hour is not one number. It is the answer to a resolution order, and the order has to be explicit or people will disagree about what was agreed.
A realistic hierarchy has at least three tiers, resolved most-specific-first: a rate negotiated for this specific project beats a rate attached to a role such as “senior engineer”, which in turn beats the workspace default. Some firms add a client-level tier between project and role. The tier count matters less than the rule that a more specific rate always wins and a less specific one never overrides it.
In Oneop’s project management module, that resolution runs project rate, then an optional role rate tier that a workspace switches on deliberately, then the tenant default. The answer is written onto the time entry at approval, and nothing downstream re-derives it.
That last clause is the whole point. If the invoice re-derives the price when it renders, then raising a rate in March changes what January’s work was worth, and your revenue figures quietly restate themselves. Freezing the resolved rate onto the entry is what makes a later rate change safe.
The failure case: forty hours that aged out
Here is the concrete version, and it is not exotic.
A four-person team delivers a discovery phase in March. Time is logged in a tracker. Billing runs monthly from a spreadsheet that the finance person assembles by exporting the tracker and filtering it. In March’s export, one engineer’s forty hours are attributed to the internal “Discovery” project rather than the client’s project, because both are called Discovery and the picker sorts alphabetically.
Nobody notices, because the spreadsheet shows a total and the total looks plausible. In June, a profitability review flags the client engagement as unusually cheap. The forty hours are found. Now someone has to decide whether to send a client a three-month-old invoice for work they were told was complete and settled.
They do not send it. Forty hours at a real rate leaves the business permanently, and the only trace is a spreadsheet nobody kept.
Notice what actually failed. Not the time tracker, which recorded the hours accurately. Not the invoice tool, which issued exactly what it was given. The failure is that no single system held the relationship between “hours delivered against this engagement” and “hours invoiced against this engagement”, so nothing could report the difference while the difference was still fixable.
Approval is a gate, not a formality
Approval exists for two reasons that get conflated. The first is quality control: a reviewer catches the twelve-hour day that was really four hours and a travel day. The second is legal and commercial: approval is the moment the business commits to a version of events it is willing to defend to the client.
Approval also has to be the moment pricing is fixed, because it is the last point where all the context is present. Afterwards, the entry should be immutable in the ways that matter. If approved time can still be edited freely, approval has not gated anything.
The failure mode to design against is the approval backlog, which is how unbilled work ages. A weekly approval cadence with a visible queue converts hours while the person who logged them still remembers the day. A monthly cadence with no queue produces June conversations about March.
Write-offs should be explicit, not silent
Some delivered hours will not be billed. A fixed-fee overrun, a goodwill gesture, an internal error absorbed rather than passed on. That is normal, and it is a decision.
The unhealthy version is the silent write-off: hours that are never converted because nobody looked, which produces the same financial outcome with none of the information. A written-off hour tells you something about scoping, about a client, or about an estimate. A forgotten hour tells you nothing and repeats next quarter.
Practically, that means unbilled time needs an age profile rather than a total. “We have 310 unbilled hours” is not actionable. “We have 40 unbilled hours older than sixty days, on two engagements” is a decision with a deadline on it.
What the conversion step has to guarantee
Turning approved hours into an invoice is a state change on the hours, not only the creation of a document. Two guarantees make it safe.
Exactly once. The conversion must select the approved entries, lock them, write the invoice lines, and mark the entries converted inside a single transaction. In Oneop, that conversion locks the entries with SELECT … FOR UPDATE and marks them converted in the same transaction, so a second concurrent attempt finds nothing left to convert and produces nothing rather than a duplicate invoice.
Posted, not exported. Sending the invoice should produce a real accounting entry, not a CSV for somebody to re-key. Oneop posts debit Accounts Receivable, credit Revenue, credit Tax through the same ledger service the rest of Finance uses, which refuses an entry whose debits and credits do not balance. If the double-entry part is unfamiliar, double-entry accounting for founders is the short version.
Two boundaries worth stating plainly. Oneop refuses to mix currencies onto one document rather than converting silently, and retainers are a tracking layer that draws down against invoices without ever posting to the general ledger, so a drawdown cannot be mistaken for revenue.
Where reporting fits, and where it does not
The number you want on a Monday morning is delivered-but-unbilled hours by engagement and by age. Building that from exports is the failure at the top of this article repeating itself in a spreadsheet, because an export of time and an export of invoices have no shared key that survives the round trip.
Be realistic about what your system reports on today. Oneop’s reporting catalogue includes a Projects dataset covering projects by status, owner, dates and budget, and reports read live rows rather than a nightly copy, so an invoice raised at 10:58 appears in an 11:00 report. Time entries are approved and converted inside the projects module rather than queried as a reporting dataset, which is worth knowing before you plan a dashboard around them.
Also worth knowing: time tracking is a Pro-plan capability, projects are available from Starter, and billing itself is a separate switch a workspace turns on deliberately, because approved time posting to a ledger has financial side effects. The pricing page has the ladder.
The part you should not automate
Automating the conversion is safe. Automating the approval is not. An approval that happens because a timer expired is not an approval, and the first time a client disputes a line, you will discover that nobody in the building can say who stood behind it. When not to automate covers the general shape of this rule: automate the mechanical step, keep the judgement step attached to a person and a timestamp.