Plan anyway

The task that gets you isn’t the one that looks hard

I’ve been working with Cursor and coding agents long enough now to have a few habits that I didn’t have at the start. This is the one that’s saved me the most wasted time, and it’s almost embarrassingly simple: if a task is even moderately complex, get the agent to make a plan first, even when you’re fairly sure you don’t need one.

Abstract illustration of navy slabs stacked unevenly on an arch over a blue painted ground

Most of the time you’re primed to do the opposite. The task looks small, you can see roughly what the change is, so you just ask for the change. The agent obliges, writes the code, and most of the time it’s fine. The problem is the times it isn’t fine, because those times are expensive.

What goes wrong when the agent skips straight to code

An agent that jumps straight to implementation will happily make a few assumptions, and some of them will be wrong. It doesn’t pause on the wrong ones. It writes a lot of code on top of them. By the time you notice, you’re not just fixing a bad assumption, you’re unpicking the assumption and all the code that was built on it.

Planning first doesn’t magically prevent bad assumptions. What it does is surface them while they’re still cheap to fix. A wrong assumption in a plan is a paragraph you can correct. The same wrong assumption in a half-built feature is a rewrite.

There’s a more subtle thing going on too. Asking for a plan forces the agent to think the task through properly before it’s allowed to change anything. It still thinks before it writes either way, but a plan makes it think harder and more deliberately, because it has to go and read the relevant code, work out how the pieces connect, and commit to an approach in writing before any of it is wrapped up in actual changes.

A real example

I’ll use an actual task. I wanted to add a third prompt-caching breakpoint to Flow Myna’s copilot. Anthropic lets you mark parts of a request as cacheable, and we were already caching the system prompt and the tools. The project schema, which is static for a given project, looked like an easy third thing to cache.

It looked simple. The schema was already being built and injected into the context, so surely you just slap a cache_control marker on it and you’re done. I’ll be honest: I nearly just asked the agent to add the feature directly. It looked like a one-liner. The only reason I didn’t is habit.

That’s the actual point of this post. The task that gets you isn’t the one that looks hard. You plan those without thinking about it. It’s the one that looks simple enough to skip the plan, because that’s the one where you don’t find out you were wrong until the code is already written.

So I asked the agent to plan it rather than do it. The first plan came back with the right general shape: split the schema into its own message, then tag that message in the CachedAnthropicModel wrapper. But partway through, the plan said this:

Wait, the schema message won’t necessarily be the first user message (history comes before it). We should search all user messages for the one containing “PROJECT SCHEMA”...

That “Wait” is the interesting bit. The agent had started writing the plan one way, noticed mid-thought that it wouldn’t quite work, and patched it. The patch was fine as far as it went. But notice what the patch is actually about: it’s solving “how do I find the schema message” by searching for it. It isn’t questioning whether tagging that message would do anything useful in the first place. The agent had spotted a small problem and not the bigger one sitting underneath it.

That’s the kind of thing that should make you pause. Not because the patch was wrong, but because a plan that’s patching itself partway through usually hasn’t been thought all the way down.

So I asked it to review the plan and check this was actually the simplest way to add the cache point.

That review is where the real problem came out, and it was the one the first plan had walked straight past. Anthropic’s caching is prefix-based: everything from the start of the request up to a cache marker has to match byte-for-byte for the cache to hit. The schema was sitting after the conversation history. History grows every turn. So the prefix leading up to the schema would change on every single turn, and a cache breakpoint on the schema would never actually hit. The first plan’s “search all user messages for the schema” fix would have correctly found the message and correctly tagged it, and the tag would have done nothing, because that message could never be a stable prefix.

The fix was to move the schema to be the first user message, before history, so the prefix is stable. That brought in a second wrinkle: PydanticAI groups consecutive user messages into one, so the schema needed a one-word synthetic assistant message after it to keep it isolated. None of that was obvious from the task as I’d originally framed it. The first plan, written quickly, had missed it entirely.

If I’d let the agent implement the first plan, it would have written real code, the eval numbers would have looked roughly fine, and I’d have shipped a cache point that did nothing. Instead I’d spent ten minutes on two plans.

The part I didn’t expect

Here’s the thing I only noticed afterwards. By the time the second plan existed, I understood our message-construction code properly. I knew the order the messages went out in, I knew that PydanticAI merged consecutive user messages, I knew exactly where the schema was injected and why that mattered for caching.

I hadn’t set out to learn any of that. The agent had to go and read process_copilot.py and config.py and trace how the AG-UI messages get converted before they hit the Anthropic API, because it needed all of that to write the plan. Reading the plan, I picked it up for free.

So planning isn’t only quality control for the agent. It’s a cheap way to understand a part of your codebase before anything changes. That’s worth a lot when you’re working somewhere you didn’t write yourself, which, with agents, is increasingly everywhere.

It's also worth keeping the plans. A plan written before the work is a better record than documentation written afterwards. After-the-fact docs describe what the code does; the plan captures why you did it this way and not the obvious way, what the constraint was, what you rejected. That reasoning is the expensive thing to reconstruct later, and it almost never makes it into a comment.

How I actually use this

It’s not complicated:

  • If a task is more than trivial, ask for a plan first, even when you think you don’t need one.
  • Read the plan. If there’s any indecision in it, any “wait” or “actually” or two competing options, treat that as a signal the task wasn’t as simple as it looked.
  • When you see that indecision, ask the agent to review its own plan before it writes anything.
  • Only then let it implement.

A second pass on a plan costs you a couple of minutes. A second pass on a half-built feature costs you the build, the unpick, and the rebuild.

None of this is novel and I’m sure plenty of people already work this way. But the gap between “plan first” and “just ask for the change” is bigger than it looks, and the tasks that punish you for skipping the plan are exactly the ones that looked like they didn’t need one.

Let me know if you do something similar, or if you’ve found a case where planning first didn’t help.

Enjoyed this post?

Subscribe to get notified when I publish something new, and unsubscribe at any time.

It's free - Substack adds a pledge button I can't turn off, but please don't use it. 😊

Subscribe on Substack