Nam Nguyen
Back to blog

Writing tickets an AI agent can finish without me

7 min readNam Nguyen

For most of this year I used AI the way everyone does. I opened a chat, described what I wanted, read the diff, argued with it, read the diff again. It worked. It also meant I had to be present for every minute of it. The agent could type faster than me but it could not move without me.

Somewhere in October I got tired of being the bottleneck in my own side projects. I was leading a team during the day and trying to ship BiBoTracking and the small tools around it at night. The limit was never the code. The limit was my attention. So I tried something different: instead of chatting, I would write the work down as tickets, and then let an agent run them one after another while I did something else.

The first attempt was bad. Not "the code was wrong" bad — "I came back and could not tell what had happened" bad.

My first tickets were wishes, not tickets

Here is roughly what my early tickets looked like: "Add a dashboard to review drafts before publishing." That is a perfectly good line in a notebook. It is a terrible instruction for something that cannot ask me a follow-up question.

What came back was a dashboard. It had features I never asked for. It had a settings page. It had empty states for cases that cannot happen. The code was not stupid — it was just answering a question I had not actually asked, which meant it filled the gaps with the most generic version of my product it could imagine. I spent the evening reading it and deleting most of it. The net result of letting the agent run unattended was that I did more work, not less.

The honest diagnosis: my ticket had no edges. I had written what I wanted to exist, not what finishing looked like. A human on my team would have come back with three questions before touching the keyboard. The agent does not come back. It commits.

The thing that actually fixed it: "done when"

The change was small and almost embarrassing. Every ticket now ends with one line that starts with Done when, and that line has to be something a machine can check without me.

Not "done when the dashboard works." Done when the list page returns 200 and shows the five seeded drafts. Done when go test ./... passes and the new test for the empty-queue case fails before the change. Done when the deploy script runs and the staging URL serves the new page.

The moment I wrote tickets like that, two things happened that I did not expect.

It forced me to make the decisions first

Writing a checkable "done when" is impossible if I have not decided what the thing does. I kept starting to write one and realising I did not know the answer — what happens to a draft I reject, is review per-post or per-batch, does a stale post keep its slug. In a chat I would have discovered those questions halfway through and answered them on reflex. Writing them down in advance meant I made each call deliberately, while I still had the whole picture in my head and no half-built code pulling me toward the easy answer.

That is most of the value, and it has nothing to do with AI. Ticket writing became the design work. The implementation was the cheap part.

It gave the agent a stopping condition

An agent without a finish line keeps going. It adds the settings page. It refactors the neighbours. It is not being greedy, it just has no way to know it is done, so it keeps producing value in the only currency it has: more code.

A verifiable check ends that. Run it, it passes, stop, commit, next ticket. The scope stops being a matter of taste and becomes a matter of fact.

The rules I ended up with

After a few weeks of running my own tickets this way, the list is short.

One ticket, one slice. If the "done when" needs an "and" joining two unrelated things, it is two tickets. Big tickets fail in the middle and leave me a half-state to untangle, which is the most expensive thing that can happen when I was not watching.

Name the files and the pattern to copy. "Follow the shape of the existing handler" removes a huge amount of invention. Most of the ugly code I got back was the agent designing something I already had somewhere else in the repo.

Say what not to touch. My repos have plenty of things that are fine but would not survive a well-meaning cleanup. An explicit "don't reformat, don't refactor adjacent code, only stage this ticket's files" line saved me more review time than any other sentence I added.

Make it log its decisions instead of asking me. This was the compromise that made unattended runs actually unattended. If a ticket hits something ambiguous, the rule is: pick the simplest sensible option, write one line about it in a shared checkpoint file, keep going. I read the log afterwards. Nine times out of ten I agree. The tenth time is a new ticket, and it costs me a few minutes rather than a blocked evening.

Keep a checkpoint file. One file that says where the project is and which ticket is next. Each finished ticket updates it. That file is what lets me come back cold, or start a fresh session, without reconstructing anything.

Nothing irreversible. Commit locally, never push. Deploy to staging, never to the box that real customers depend on. The agent runs unattended inside a fence where the worst outcome is a bad local commit I can throw away.

What it cost, and what I would do differently

The cost is real and I want to be honest about it. Writing tickets this way is slower than chatting. A ticket I can hand over unattended takes me noticeably longer to write than a prompt, and for small changes that trade is simply not worth it — if the work is one file and ten minutes, I still just open a chat and do it with the agent in front of me.

The trade pays off on anything with a sequence. Five or six tickets that depend on each other is where I get an evening back, because the whole chain runs while I am not there.

I also wasted time early on by giving the agent too much freedom and then reviewing deeply anyway — the worst of both worlds. I had neither the speed of trusting it nor the quality of writing the code myself. Now the rule is: if the check passes and nothing outside the ticket's files changed, I skim and move on. If the check fails, that is my signal to read properly. Earning that discipline took a couple of rounds of me re-reviewing work that was already fine.

What I would do differently: I would have written the "done when" line from day one instead of discovering it after throwing away a dashboard. And I would have started with smaller slices. My instinct when writing tickets is still to make them too big, because a big ticket feels more productive to write. It is not. It is just a bigger thing to debug when it fails in the middle.

This is the same lesson I keep relearning in different clothes. When I handed product strategy to AI the failure was not that the output was bad — it was confident, fluent and untethered from anything I could check. Ticket work failed the same way, for the same reason, and got fixed by the same thing: a check I did not write the answer to. If you want the running tally of what actually came out the other end, that is in what I shipped in October 2026.

The summary I would give myself in September: the agent is not waiting for a better prompt. It is waiting for a finish line. Writing good ai coding tickets is mostly the old, unglamorous work of deciding what "finished" means — and the agent is just the thing that holds you to it.