One of the changes in how I work with coding agents is that I spend less and less time actually writing code. That does not mean a larger change suddenly takes little time. That time has mostly moved earlier.
In a recent change I had to figure out why users sometimes had to wait a long time after a successful payment before their order was processed. Payment went through an external payment provider. After paying, the user was sent back to our application via the browser, after which processing continued. If the user interrupted that browser flow, our application never received that confirmation. A background job would eventually clean it up, but in the worst case someone could wait about half an hour while the money had already been charged.
My instruction to the agent was deliberately not to solve this yet. I first wanted to explore which options existed to make confirmation more reliable and faster.
A few minutes later there was already a fairly concrete plan. Use a webhook from the payment provider as the primary confirmation, treat the existing return URL mainly as part of the user experience, reuse the existing completion flow, and run the background job more often as a safety net. The existing guards in the application would ensure those different routes did not get in each other's way.
If I had given the go-ahead to execute that plan at that moment, working code would probably have come out fairly quickly.
Instead, I spent a large part of the afternoon talking it through.
A plan does not have to be wrong to not be good enough yet.
I think that is important to say. The problem with the first proposal was not that the agent produced nonsense. On the contrary. The solution was technically plausible, fit the existing application, and was heading in the right direction at a high level.
But a plausible solution is not yet a design I want to take responsibility for.
The plan introduced a webhook alongside the existing return URL, for example. That is logical: the return URL depends on the user's browser, while a webhook arrives server-to-server. What I mainly wanted was for the plan to include the consequences of that choice.
As soon as both routes can process a payment, you have two independent processes that can try to do roughly the same thing at roughly the same time. And really there were three, because the background job was still there as well.

The first plan assumed the existing guards offered enough protection against double processing. I pushed the agent back on that. Different processes could process the same payment, so what happened when two of them read the same status at almost the same time?
Walking through the existing code showed that race was indeed possible. Two processes could both conclude that the payment still needed to be processed and then both continue.
The design question had therefore changed. We were no longer only looking for a faster way to receive a payment confirmation. We had to determine how multiple independent processes could safely process the same payment.

I do not use Plan Mode as a place where I write an elaborate prompt, get an architecture back, and then press Build. It is more like a design review.
The agent makes a proposal. I then try to surface the assumptions in that proposal. I ask questions about choices and consequences, have relevant code investigated further, and ask for alternatives when a choice does not convince me. When external systems are part of the change, we also pull in the documentation.
Meanwhile the plan changes with us.
The agent does not have to come with a bad proposal for my input to be needed. Especially a plausible proposal needs to be tested against the things that have not been included yet.
That happened, for example, with the status of a successful payment. One of the proposals was to effectively treat Succeeded as a final status.
That did not solve the race condition. Two processes could still both read the old status before either had written Succeeded. I also pointed out that a payment can still change after Succeeded. A refund is also a valid later status change. So we would not solve a race and at the same time block an existing flow.
The proposal was understandable, but my input was needed to hold it against the full lifecycle of a payment.
Once it was clear that the race had to be solved in a different way, the agent came up with locking. That too was a logical proposal on its own.
I only pointed out that the same payment data was being processed from different processes. An in-memory lock protects one process against concurrent processing within that same process. Another process knows nothing about that lock and can load the same payment in the meantime.
So the concurrency protection had to live at a level shared by all the processes involved, for example through optimistic concurrency on storage.
There was another complication. Successful processing did not only mean our own database was updated. An external backend system also had to be called, and that call was only allowed to happen once as well.
So it was not enough to protect only the writing of the local status. The right to perform the external processing also had to be part of the same concurrency and idempotency solution.
That immediately raised the next design question: what happens when a process claims the payment, but the update to the external system then fails? When may another process retry? How do we prevent both double processing and a payment that, after a temporary failure, is never picked up again?
Those kinds of questions changed the plan step by step. A locally good solution had to be tested each time against the rest of the flow.
A similar moment arose around where the webhook should come in.
The agent reasoned from the existing payment logic and proposed processing the webhook there. Technically an obvious place: that was where the code lived that knew how payments should be processed.
Only that service was internal. The external payment provider could not reach it at all.
Human knowledge of how the application was actually deployed and reachable made clear why the proposal could not work directly. The webhook had to arrive on a publicly reachable part of the system and from there reach the internal payment flow in a controlled way.
During the same planning phase we also pulled in the payment provider's documentation. Which events are actually sent? Which status fits our flow? How is a message signed? How do retries work? What happens when our application is temporarily unreachable? Which assumptions are we making about the API version in use?
Not every question changed the design. Some confirmed that a proposal from the agent was fine. But then it had become a controlled choice instead of an implicit assumption.
So the questions I ask in a session like that are not meant to have the technique explained to me. I use them to complete the plan and test proposals against information the agent has not included yet.
Sometimes that information comes from further investigation in the code or documentation. Sometimes I provide the extra input the design needs myself.
Meanwhile the agent can quickly work out alternatives, search through the code, and investigate the consequences of a choice further. That is exactly what makes such a conversation valuable for me. I do not have to fully work out every alternative myself in order to evaluate it.

I do not use Plan Mode only to see what an agent wants to build. I use it to combine its proposals with the knowledge and trade-offs for which I, as a developer, remain responsible.
In traditional development, design and implementation often run into each other more. You start on a solution, discover while coding that an assumption is wrong, change the design, and continue. Writing code is thereby partly also a way of thinking through the problem.
A coding agent removes some of that natural resistance. If I approve a plausible plan, an agent can move through a large part of the implementation in a short time. That is exactly what I want to use it for, but it means I automatically get less of the time I used to have while programming to deepen my understanding of a design.
I therefore try to move that time earlier on purpose.
On a larger change I sometimes read a plan multiple times. If the discussion has become long, I have the plan summarized and read the new version again. I regularly use a simple answer mode to prevent every design question from resulting in pages of text. That keeps the conversation iterative without losing the main thread.
My criterion for a go-ahead is not that the plan is perfect. I want to understand what will be built and why, which parts of the system are touched, which important trade-offs were made, and which risks we consciously accept. Uncertainties may remain, as long as they are consciously in the plan instead of lying invisible underneath it.
For this change, creating and understanding the plan ultimately took a large part of an afternoon. The implementation afterward went relatively quickly.
That is a ratio I increasingly arrive at with coding agents. Producing code becomes cheaper and faster, but a wrong design decision does not. An agent can implement a misunderstood design very efficiently across a large part of the codebase.
That is why I find the time before implementation increasingly important. Not because everything has to be locked down in advance, but because this is the moment when I can still cheaply ask questions, explore alternatives, and reject or adjust proposals.
That was not delay before the real work could begin. That was an important part of the work.
That is, for me, the essential difference with vibe coding. Not that the agent writes less code. After my go-ahead he is allowed to write a lot of code. The difference is that before that go-ahead I want to understand which solution I am having built, which trade-offs are in it, and where human knowledge was needed to complete the proposal.
An approved plan is still not yet a good implementation. On a larger change such a plan can span dozens of files and different responsibilities. If I give that all at once to one agent, I expect quite a lot: that he holds context throughout a long implementation, does not mix responsibilities, and keeps delivering the same quality everywhere.
That is why my workflow changes again after the go-ahead. Instead of letting one agent execute the full plan, I split the implementation into bounded pieces with their own responsibility. Where possible, agents can even work on those in parallel.
The next article is about that step: how I turn an approved plan into small, focused implementation assignments, so agents can work as independently as possible without the quality of the execution diluting.