Skip to content
← All guides

Writing a prompt that produces working code the first time

6 min read

The five parts of a prompt that lands, why pointing at an existing file beats describing a pattern, and a template you can paste today.

You asked for rate limiting. What came back was a tidy in-memory Map of counters with a sliding window, decent variable names, and a test that passed. It resets on every redeploy, and since you run three instances behind a load balancer, each one enforces its own limit independently.

The code was not wrong. The prompt was. "Add rate limiting" describes a category of solution, and the agent picked the most common member of that category, which happens to be the one that only works on a single process that never restarts.

The five parts of a prompt that lands

A prompt that produces working code on the first attempt usually contains five things. Not five paragraphs. Five facts.

Where in the repo, named as a path. Not "the API layer" but src/middleware/rateLimit.ts. The moment you name a file, the agent stops guessing at your structure and starts reading.

The change, in behavioural terms. What should be true afterwards that is not true now.

The constraints that are not obvious from the code. Multi-instance. No new dependencies. Must work behind the existing proxy. These are the facts that live in your head and nowhere in the repository, which is exactly why they have to be in the prompt.

The definition of done, expressed as something runnable. A command and an assertion.

What not to touch. The schema, the public API surface, the vendored files.

Here is the prompt that produced the broken limiter:

Add rate limiting to our API. Something reasonable, like 100 requests
per minute per user. Make sure it's tested.

And the rewrite:

In src/middleware/rateLimit.ts, add per-API-key rate limiting to the
Express app: 100 requests per rolling minute, keyed on req.apiKey.

Constraints: we run 3+ instances, so state must be shared. Use the
existing Redis client exported from src/lib/redis.ts. No new deps.
Return 429 with a Retry-After header in seconds.

Follow the middleware shape in src/middleware/auth.ts.
Do not modify src/routes or the Redis connection settings.

Done when `pnpm vitest run src/middleware/rateLimit.test.ts` passes,
including a case where two middleware instances share one Redis mock
and the 101st request across both returns 429.

The second one is not better because it is longer. It is better because every sentence removes a decision the agent would otherwise have made on your behalf.

Point at a file, not at an idea

If you take one thing from this article, take this: the highest-leverage sentence you can add to any prompt is "follow the pattern in src/services/billing.ts".

Describing a convention in prose is lossy. You will write "use our standard error handling" and the agent will produce something reasonable that is not yours. Pointing at a file is lossless. The agent reads it, sees how you construct errors, how you name things, whether you use classes or plain functions, how you handle the transaction boundary, and what your import ordering looks like. All of that transfers without you writing any of it down.

This also fixes the consistency problem that shows up around week two, when the third service written by an agent looks nothing like the first two. Pick the file you consider exemplary and reference it every time.

Paste the error, not your summary of it

When something fails, the instinct is to describe it. "The build is failing on the types in the auth module." That is a paraphrase, and paraphrasing throws away the two most useful things in the output: the exact symbol and the exact line.

Give the verbatim text and the command that produced it.

$ pnpm tsc --noEmit
src/services/session.ts:42:7 - error TS2322: Type 'string | undefined'
  is not assignable to type 'string'.
    Type 'undefined' is not assignable to type 'string'.

42       const userId: string = payload.sub;
               ~~~~~~

An agent given that will go to line 42 and reason about the actual type. An agent given "types are broken in auth" will search, guess, and quite possibly refactor something that was fine. The same applies to failing tests, stack traces, and runtime logs. Copy the text.

Ask for a plan when it touches more than two files

For a single-file change, just ask for the change. For anything spanning more than two files, ask for a plan first, in prose, with no code.

The reason is not that the agent plans better than it codes. It is that a wrong plan costs you thirty seconds to read and one sentence to correct, whereas a wrong implementation costs you a review of four hundred lines you now have to either fix or throw away. You are moving the correction point earlier, where it is cheap.

Then commit the verification step up front, in the same prompt, so the agent checks its own work before handing back:

Before you start, list the files you will change and why, in under
150 words. Wait for my go-ahead.

After implementing, run in order and paste the output:
  pnpm tsc --noEmit
  pnpm eslint src/middleware --max-warnings=0
  pnpm vitest run src/middleware
If any command fails, fix it and re-run before telling me you are done.

That last line matters more than it looks. Without it you get "I've implemented the change, you may want to run the tests", which is the agent handing you back the part of the job you delegated.

Say what not to do

Negative constraints do a lot of work because they close off the failure modes you have already seen.

  • No new dependencies. Otherwise you get a small utility library added for one function.
  • Do not modify the schema or write a migration. Contain the change to code.
  • Do not create new files; put it in the existing module. The default drift is towards more files than you want.
  • Do not change unrelated formatting. Keeps the diff readable.
  • Do not touch the tests, only add to them. Stops a failing test being "fixed" by deletion.

One more rule, and it is the one most often broken: one task per prompt. Never bundle a refactor with a feature. When you ask for both, you get a diff where the new behaviour and the moved code are interleaved, and you cannot review either without untangling them first. Refactor, verify it is green, then build on top.

A template, and when it is the wrong answer

File(s): <path, or "start in <path>">
Change: <what should be true afterwards>
Pattern: follow <path to exemplary file>
Constraints: <runtime facts not visible in the code>
Do not: <files, deps, schema, formatting>
Done when: <exact command> passes, including <specific assertion>

That fits in six lines and covers most of what you hand to an agent day to day.

Now the caveat. If you find yourself writing four hundred words of context before the agent can start, stop and read what you have written. That length is a signal, not a badge of thoroughness. It usually means the task crosses too many boundaries, or that the context you are typing out by hand belongs in the project instruction file where every session gets it for free, or that the code itself is tangled enough that no one could explain the change briefly. Fix the scope or fix the repo. Do not fix it by typing more.

Before you press enter, check five things:

  1. Is there a file path in the prompt?
  2. Is there a file to imitate?
  3. Is the definition of done a command someone could run?
  4. Have you said what must not change?
  5. Is this genuinely one task?
promptingworkflowai-agentscode-quality