Prompt Anatomy
An agent given an underspecified task does not stop and ask — it fills every gap with the statistically most plausible choice. Some of those choices will be wrong for your system, and they will be wrong silently, inside a diff that compiles and passes its own tests. A production-grade prompt is not "more detail"; it is a specific set of components, each of which closes a class of gap.
Six components. Not every task needs all six, but you should know which one you're omitting and why.
The six components
1. Intent
What you want and why. The why matters because agents make dozens of micro-decisions you didn't anticipate, and intent is the tiebreaker they use. "Add CSV export so our accountants can reconcile invoices in Excel" produces different escaping, encoding, and column choices than "add CSV export" — Excel compatibility implies BOM-prefixed UTF-8 and ; handling that a bare request never surfaces.
2. Context pointers
Where to look before writing anything: the files that define the pattern to follow, the module being changed, the existing utility the agent would otherwise re-implement. Pointers, not prose — see src/routes/invoices.ts for the route pattern beats three paragraphs describing your route conventions, because the agent reads the file and copies real structure instead of your lossy summary. Repo-level context belongs in CLAUDE.md; the prompt carries only task-local pointers.
3. Constraints
Non-negotiables the agent cannot infer: no new dependencies, must stream (never buffer the full dataset), keep the public API stable, stay on the ORM (no raw SQL). Constraints are cheap to write and expensive to omit — one missing "no new dependencies" line is how csv-stringify, dayjs, and a second HTTP client enter your lockfile in a single PR.
4. Acceptance criteria
Observable, checkable statements of done. Not "works correctly" — "GET /invoices/export?from=2026-01-01 returns 200 with Content-Type: text/csv, one row per invoice line item, and a test covers an invoice containing a " in the customer name." Acceptance criteria are what you verify against during review; without them you're reviewing vibes. They are also the only defense that makes scope fences enforceable — see Control Techniques.
5. Out-of-scope
What the agent must not touch, stated explicitly. Agents "helpfully" refactor adjacent code, rename inconsistent variables they pass by, and upgrade patterns they consider dated. Every out-of-scope line is one less 40-file diff to untangle. Typical entries: don't modify existing endpoints, don't reformat untouched files, don't change the DB schema, don't touch auth.
6. Process directives
How to proceed, not what to build: plan first and wait for approval, ask questions before coding, implement in reviewable increments, list your assumptions. These control the interaction loop and are the highest-leverage lines in any risky prompt. Claude Code has plan mode (Shift+Tab) built for exactly the first one. The full pattern set is in Control Techniques.
Worked example: "add CSV export"
The task: an invoicing service (Node/Express/Postgres, ~40k LOC) needs invoice export.
Weak prompt — the version that gets typed at 4:50pm:
Add CSV export to the invoicing app. Users should be able to download
their invoices as a CSV file. Add a button in the UI too.
Make sure it handles errors properly.
Use sensible defaults for the columns.
Keep the code clean.
Don't break anything.
Thanks.Eight lines, and the agent must now invent: the endpoint shape, auth behavior, which columns, date/currency formatting, line items vs. invoice-level rows, pagination vs. streaming, filename, escaping rules, where the button goes, and what "errors" means. That's ten decisions, each ~80% likely to be acceptable — so the odds the whole diff needs no rework are under 15%.
Strong prompt:
## Intent
Add invoice CSV export so accountants can reconcile in Excel.
Excel compatibility is the priority (UTF-8 BOM, correct quoting).
## Context
- Route conventions: src/routes/invoices.ts — follow its auth middleware
and error-response pattern exactly.
- Invoice model + line items: src/models/invoice.ts
- We already have a streaming helper: src/lib/stream-response.ts — use it.
## Requirements
- New endpoint: GET /api/invoices/export?from=<date>&to=<date>
- Scoped to the authenticated user's organization (same guard as
GET /api/invoices).
- One CSV row per line item: invoice_number, issued_date (YYYY-MM-DD),
customer_name, item_description, quantity, unit_price, line_total,
currency, status.
- Stream the response; never load all invoices into memory
(some orgs have 200k+ line items).
- Filename: invoices_<from>_<to>.csv via Content-Disposition.
## Constraints
- No new dependencies. Hand-roll the CSV escaping (quote fields
containing " , or newline; double embedded quotes).
- Do not change the Invoice model or any existing endpoint.
## Acceptance criteria
- Integration test: export with 3 invoices incl. one customer name
containing a double quote and a comma; assert exact CSV output.
- Test: date range excludes invoices outside [from, to].
- Test: user in org A cannot export org B's invoices (403 or empty —
match whatever GET /api/invoices does).
- Opens cleanly in Excel (BOM present, no mangled accents).
## Out of scope
- No UI changes in this task — endpoint only, UI is a follow-up.
- No XLSX, no PDF, no async job queue.
## Process
Before coding: read the three files above, then list any questions
and your assumptions. Wait for my go-ahead, then implement endpoint
+ tests in one pass.What the strong version actually prevented — each line maps to a real invented decision:
| Invented decision the weak prompt allows | Line that closes it |
|---|---|
| Buffers all rows into memory, OOMs on the 200k-line-item org | "Stream the response; never load all invoices" |
Adds csv-stringify + dayjs to package.json | "No new dependencies" |
| One row per invoice with line items JSON-encoded into a cell | "One CSV row per line item" + column list |
| Rolls its own auth check, subtly different from the middleware | "same guard as GET /api/invoices" |
| Silently exports across organizations | The org-scoping acceptance test |
| Half-builds a React export dialog you didn't want yet | "No UI changes in this task" |
Excel shows é instead of é for every French customer | "Excel compatibility… UTF-8 BOM" |
The strong prompt took ~6 minutes to write. The weak prompt's rework loop — review, discover the buffering, discover the dependency, re-prompt, re-review — is a 2–3 hour tax, paid with interest because you now also have to check the fixes didn't regress anything.
How much anatomy does the task need?
Full anatomy on a typo fix is theater. Scale structure to blast radius:
| Task risk | Example | Components needed |
|---|---|---|
| Trivial, reversible | Fix typo, rename local var, bump a log level | One line. Intent only. |
| Small, contained | Add a validation rule, new unit test, tweak one endpoint | Intent + context pointer + 1–2 acceptance criteria |
| Medium, touches shared code | New endpoint, new UI component, refactor one module | Intent + context + constraints + acceptance + out-of-scope |
| Large or irreversible | Schema migration, auth change, cross-cutting refactor | All six — and process directives are mandatory (plan first, incremental). Consider a full spec instead. |
The line between "big prompt" and "spec" is durability: a prompt is disposable, a spec is a repo artifact that outlives the session. When acceptance criteria exceed ~10 items or multiple sessions will touch the work, move it to a spec file — see Prompts vs. Specs.
Related
- Prompts vs. Specs — when a task outgrows a prompt and needs a durable spec.
- Control Techniques — the process-directive patterns in copyable form.
- Implementation Prompts — this anatomy applied to feature, backend, and UI work.
- Writing Effective Specs — the same components scaled up to multi-session work.
- Prompt Library: Implementation — copy-paste prompts built on this structure.
- Why Agents Fail — the failure mechanics that make each component necessary.