
At a glance
The problem
Bujeti already had categories. They were labels attached to expenses, and the default set that shipped with every account could not be tracked at all. A finance lead could tag a transaction “Travel” and then have no way to ask what Travel had cost this month, who was spending it, or whether it was over.
So the work happened somewhere else. Teams exported transactions, re-categorised them in a spreadsheet at month end, and made decisions from that copy. The categorisation inside the product was not wrong so much as inert, and the data quality problem that showed up in reporting was really an incentive problem: nobody categorises carefully when the category does nothing.
I was handed a request for finer granularity, which reads as a request for deeper folders. Granularity was not going to fix anything on its own. A category nobody trusts is a category nobody fills in accurately, at any depth.
Research and approach
I ran sessions with finance users and internal stakeholders, then studied how other platforms model categories: accounting tools, spend management products, and the chart-of-accounts structures these teams already keep outside the product.
Two things came back that shaped everything after:
Finance teams were not waiting for us to give them categories. They had a structure already, in a spreadsheet or an accounting package, and any product that made them retype it was going to lose.
Miscategorised spend was not carelessness. People categorise well when something depends on it, and they categorise badly when the field is decoration.
The decision: a category is a control, not a label
I redefined the category from a tag into an object that owns things. A category now carries a spend limit, an approval rule, a set of categorisation rules, its sub-categories, and every transaction that has landed against it. That single change is why it moved in the navigation and now sits under Compliance, beside approval rules and policies, rather than filed under expenses.
The list view carries the argument. It leads with Amount spent and Available, so the first thing a category tells you is how much room is left in it, not what it is called.
A category that owns limits and rules is a heavier object to create, and setup got slower for a small team who only wanted five labels. It also put category management behind an admin permission, which means a spender can no longer invent a category mid-expense. I took that trade because uncontrolled category creation is the thing that destroys the reporting the feature exists to produce.

Three ways in, because setup is not one situation
Making the category heavier made the first run at it harder, and research had already told me the taxonomy usually exists somewhere else. So creation is not one flow. It is three, each aimed at a different starting state.
The everyday case. Someone needs one more category, or one sub-category under an existing parent, and should not have to open a bulk tool to get it.

An editable grid with Add a category and Add a sub category as inline row actions, sub-rows indented under their parent, and a running count at the bottom so the person can see the shape of what they have built. Limit is a column, so the control is set at the moment the category is created rather than as a second pass nobody comes back for.



The import maps the customer’s column headers onto Bujeti’s fields, with their own sample data shown beside each mapping so they can confirm the match rather than trust it. For anyone without a file ready there is a pre-formatted template to download, which quietly teaches the required shape instead of rejecting a bad upload later.


The limit, and the buffer under it
A hard limit fails in a specific way: it stops a legitimate purchase at the worst possible moment, usually with someone standing at a counter. No limit fails the other way, quietly, and only shows up in the month-end report.
So the limit is stated with an explicit buffer beside it, and the progress bar shows three zones rather than two: what has been spent, what remains inside the limit, and the declared overage the business has already agreed to absorb. The category is still controlled, and going slightly over is a visible, sanctioned state instead of a failure.
Everything the category owns is on this page as a tab: transactions, sub-categories, approval rules and categorisation rules. Insights sit under the limit, cutting the same spend by top sub-category, by trend and by person.

Rules are what actually moved data quality
Research said accuracy would not improve by asking people to try harder, so the system had to do the categorising. A categorisation rule matches on transaction description or narration and assigns a category automatically, with any or all of its conditions required, and each condition using an explicit operator rather than a hidden fuzzy match.
I kept the rule readable on purpose. Someone in finance has to be able to look at a rule six months later and understand why a transaction landed where it did, which is also what makes a wrong rule fixable rather than mysterious. Rules run alongside the manual path, they never replace it, and the category’s transaction list is where a bad assignment gets caught.

Where the category meets the money
The transactions tab lists what has actually hit the category: counterparty, amount, the person who spent it, the account, status, and whether a receipt is attached. The receipt column carries an add affordance where the file is missing, which turns a reporting gap into a one-click fix at the row where it happened. Past transactions can be pulled into a category as well as future ones, so adopting the feature does not mean starting the history over.

Editing has to work at the same scale as creating
A structure imported from a CSV is a structure that will need correcting in bulk, so selection and multi-edit mirror the creation table rather than forcing a per-row modal. The single edit stays small and focused for the one-field change, which is the far more common case.


How I worked
- Translated PRDs into a category model, taking the request for deeper hierarchy and returning a definition of what a category owns
- Planned and ran feature testing with users and engineers, validating the creation paths before any of them were built out
- Partnered with customer success and marketing on launch, since a feature that changes where something lives in the navigation needs telling, not discovering
- Drove roadmap prioritisation by arguing the rules engine up the order, because without automation the accuracy target was not reachable
Outcomes
improvement in expense data quality, which is the number the rules engine was built to move
feature adoption inside the first quarter, on a feature that requires setup before it returns anything
Reflection
The useful lesson here was about reading a request. I was asked for granularity, and granularity was a real need, but shipping only that would have produced a deeper version of a field people already ignored. The work got good when I went after why the field was ignored instead.
The second lesson was that data quality is a systems problem wearing a behaviour problem’s clothes. Every intervention that worked here removed a human decision or gave one a consequence. None of them asked anyone to be more diligent.
36% adoption in a quarter is decent for a feature with a setup cost, and I still do not know what stopped the other 64%. I never instrumented which creation path people chose, or how many started setup and abandoned it. If CSV import was carrying adoption, that is where the next investment goes. If nobody found it, that is a different and cheaper problem. I built three paths and shipped without the means to learn which one earned its keep.