Forty-seven open tickets, one morning, four hours. We made about ten real decisions. The other 37 we touched without deciding. We read them, scrolled, opened a second tab for context, opened a third tab for scope, lost our place, and moved on. By the time we had enough loaded to make a call, we’d spent the attention the call was supposed to get.
The tab-thrash was the slow part. The triage itself wasn’t.
The math on one ticket
Take a single actionable ticket. Someone files a three-sentence report: a page is broken for one of their events. To decide what to do with that, we need the ticket itself, the page in the admin to see the breakage, a linked parent ticket to understand scope, and sometimes a quick field-state lookup to confirm what the data says. Four surfaces. Each one click away. Each click breaks the scanning rhythm and forces a context rebuild when we come back.
Multiply that across the whole list and the morning is gone. It went to navigation, to losing our cursor in a list of rows, to re-reading a ticket we’d already read because the tab we opened in between flushed it out of our heads.
We’d run triage this way for years and assumed the cost was the analysis. Read the report, work out whether it’s a two-hour fix or a two-week rewrite, decide. That judgment felt like the expensive part, so the navigation overhead hid behind it. The two were fused into one motion: read, think, decide.
What split the motion in two
Then we started handing the investigation pass to Claude Code. Point it at a ticket and it reads the report, traces the relevant code path, inspects what the linked page actually renders, and comes back in about 30 seconds with a verdict and a rough effort estimate. Two-hour fix or two-week rewrite, with the reasoning.
That collapsed the analysis cost to near zero, and the motion came apart. Reading and deciding were never one operation. They only looked like one because both were slow and they happened back to back. With reading down to 30 seconds, deciding still meant the same five-tab dance, except now the dance was 95% of the time per ticket instead of half of it.
None of this is specific to JIRA or to us. AI moves a bottleneck instead of removing it. Whatever was second-slowest is now first. If a fast-thinking step feeds a slow-acting step, accelerating the thinking step puts all the pressure on the acting step. The win you expected doesn’t show up, because the constraint relocated while you were celebrating.
Investigation got cheap. Decision didn’t. Our decision UI was a backlog page that could show us a ticket existed but couldn’t let us do anything to it without leaving, and it became the choke point.
Build the verbs
The fix was to turn the backlog page into an action surface. We rebuilt the project-manager home page so that each row carries its own context inline: the ticket notes, the comment stream, the linked child tickets, and a RICE breakdown that the AI investigation pass pre-populates. Pulling data onto a page is the easy 20%, and it is not where the leverage was.
The leverage was in the verbs. The page had to let us act without navigating away:
- Accept a ticket into the active queue.
- Comment without opening a new tab.
- Reroute to a different component.
- Park it.
One click per decision. The page stays intact underneath. No full reload, no route change, no confirmation dialog that scrolls us away from our place in the list. The write goes out to JIRA over the REST API and the row updates in place. Where JIRA demands metadata for a transition, a priority change, a status move that requires fields, the page surfaces a small inline form right there in the row: pick the transition, fill what’s required, confirm. Still no navigation.
The triage motion that came out the other side: read the pre-loaded verdict, decide in ten seconds, click one button, drop to the next row. No tabs. No lost place. The 37 tickets we used to touch without deciding became decisions, because deciding stopped costing four context switches.
A gorgeous read-only dashboard that still makes you leave to act is a slightly nicer version of the bottleneck. It shows you the verdict faster and then strands you. On top of a fast AI analysis layer, the verbs are the product.
The dead button
Our first version of the action endpoints did what looked like the correct thing: on a bad request they returned a 4xx with a clean JSON error body. In the browser, the button clicked and nothing happened. No toast, no console error we could see, just a dead button. It ate an hour because nothing surfaced.
The cause was IIS. Depending on how httpErrors is configured, it replaces a non-200 response body with its own HTML error page. That’s fine for a human hitting a broken URL and poison for a JSON API on the same host. The browser was getting HTML, our JavaScript was trying to parse it as JSON, it threw silently, and the button did nothing with zero feedback.
So we made every action endpoint return HTTP 200 and carry success or failure in the body: a status field of ok or error, with a message on the error case. The client reads status before treating the response as a win.
That is not the design we’d pick on a clean system. It’s the design that works on the system we have. It also forced a discipline we’d have been sloppy about otherwise: every action endpoint now has an explicit success/failure contract in its payload, independent of the HTTP layer. When the framework silently eats an error response, a one-click action surface turns back into a guess-and-refresh surface, so the verbs have to report back reliably.
The 30-minute change that beat the features
The highest-leverage change that day wasn’t the action endpoints or the inline RICE. It was a 30-minute data-model split.
We had one status, Parked, doing two jobs. Sometimes Parked meant “we deliberately deprioritized this, it’s ours to pick back up whenever.” Sometimes it meant “we’re blocked waiting on someone else to do something.” Those look identical in a list and they are completely different. In one we own the next move; in the other we don’t. Lumping them together meant the question that drives a whole triage session, “what do we actually work today,” had no clean answer.
So we split it. Parked means we own the next move and chose not to take it yet; it gets a reason note. Waiting means they own the next move; it gets an idle timestamp so we can see how long it’s been stuck. Now “what do we work today” has a definition the page can compute: everything active, plus any Waiting ticket older than 48 hours. That filter is only expressible because the two states are separate.
Thirty minutes of modeling beat every shiny feature we shipped that day, because it modeled intent, who owns the next move, instead of how the ticket looks in the list.
Measure decisions per hour
Tickets viewed per hour went through the roof the day Claude started doing investigation. That number is a vanity metric. It went up precisely because we were viewing tickets without deciding on them, which is the failure, not the win.
The number that matters is decisions per hour. Before we put the verbs inline we were at roughly ten in four hours of active triage. After, with the verbs inline and the page intact underneath, the whole backlog cleared in a fraction of that time, because each decision cost one click instead of four context switches.
When AI makes analysis fast, look at the next-slowest step. For us it was a page that let us act on a ticket without leaving it, plus one status that had mixed “we own the next move” with “someone else does” and had to become two.
Related
- Trading Badge Soup for One Human Sentence: collapsing status complexity into one actionable signal per ticket
- When Working Notes Become Operational Infrastructure: how AI transforms personal workflows into structured tooling
- An Evening of Progressive Disclosure: Five Commits to Hide Everything Behind ‘Details’: collapsing UI complexity so each row stays scannable
- I Was About to Write a daily_digest.py. The Platform Already Shipped It.: building tooling on top of existing platform capabilities