For two years, the internet was obsessed with the perfect prompt. Threads full of magic phrases. “Act as a senior engineer.” “Take a deep breath and think step by step.” Whole communities formed around coaxing better answers out of a model with cleverer wording.
That era is quietly ending. Not because prompting stopped working, but because the thing that actually determines output quality turned out to be one level up. As AI coding tools graduated from autocomplete into agents that read your whole repository, open pull requests, and reason across systems, the bottleneck moved. It’s no longer how fast a model can write code. It’s how clearly you can express what you actually want.
That skill has a name now: spec engineering, which means writing precise, structured specifications that a human and an AI agent both treat as the source of truth. It’s the discipline behind what teams call spec-driven development (SDD), and over the past year it’s gone from a niche practice to arguably the most important new engineering habit of the AI era.
This post covers what spec engineering is, why it beats prompting on anything non-trivial, how to write a spec that works, and a complete, copy-pasteable example you can adapt today.
Prompt, conversation, spec: three different things
It helps to be precise about the ladder, because people blur these together.
A prompt instructs a model to produce a response. Asking for output. That’s prompt engineering.
A conversation is you steering an agent turn by turn, saying “refactor this,” then “now add null checks.” That’s interactive coding, or what most people mean by vibe coding.
A spec formulates your intent as a set of requirements, constraints, and acceptance criteria that the agent then decomposes into steps and implements.
The cleanest analogy: prompting is asking someone to “make dinner.” A spec is handing them a recipe. Both can produce a meal, but only one of them reliably produces the meal you wanted, is repeatable next week, and can be handed to a different cook with the same result.
The deeper shift is from micromanagement to delegation. With prompts you babysit each task. With a spec you hand over a contract and let the agent do the bulk of the writing while you steer and verify.
Why prompting hits a wall
Prompting works beautifully for a button. It falls apart on a real project, for three reasons.
The ambiguity tax. Every vague requirement becomes a coin flip. Ask an agent for “an API that returns a user’s orders” and you’ll get something reasonable, but the field names, the pagination shape, the error format, and the auth handling will be whatever the model guessed today. Multiply that across a codebase and you’re paying in rework, inconsistency, and quiet bugs. A spec pays that tax up front, once, on purpose.
Context amnesia. Agents don’t remember your architecture. Ask one to touch your auth middleware and it may cheerfully invent an error-handling pattern that clashes with the rest of your app, because nothing told it your patterns. Re-explaining the architecture in every prompt is exhausting and lossy. A spec is persistent context that every agent can share.
Prompts are disposable; specs are durable. A great prompt works once. Tomorrow a teammate faces the same task from scratch. A month later even the author can’t remember the phrasing that worked. Specifications, by contrast, can be versioned, reviewed, referenced, and reused. And because a good spec communicates intent rather than model-specific tricks, it keeps working when the next model drops, so you don’t have to relearn prompting every quarter.
The tell that you’ve crossed from prompting into spec-writing is when your “prompt” starts defining behavior, constraints, integration points, and validation criteria. At that point it isn’t a prompt anymore. It’s a small spec, and it’ll work with any model, because it isn’t leaning on tricks.
The anatomy of a good spec
A spec is not a novel, and it’s not a legacy 80-page requirements document. It’s a structured description of intent. The sections that matter most:
- Objective. What you’re building and why. The “why” lets the agent make sensible calls on things you forgot to specify.
- Scope and non-goals. An explicit list of what you are not doing. This is the single highest-leverage section and the one people skip.
- Context and constraints. The tech stack, existing patterns, libraries, and architectural rules the work must respect.
- Behavior. Inputs, outputs, states, and the exact shape of the contract (data models, API responses, UI states).
- Edge cases. The failure modes and weird inputs. This is where senior judgment lives.
- Acceptance criteria. Testable conditions for “done,” ideally phrased so tests can be derived directly from them.
- Boundaries and guardrails. What the agent may do freely, must ask about, and must never do.
A useful rule of thumb from teams doing this at scale: write authoritatively, in plain natural language, and only omit what the model can reliably infer. Experiments running the same build through a spec tool repeatedly found that dropping the authoritative framing made the agent treat requirements as optional and skip whole features; that structured-but-rigid formats like raw JSON produced worse results than clear prose; and that leaving out genuinely novel details, like a custom schema or an unusual library, blew up debugging time later. Give detail where the model can’t guess, and keep it brief where it can.
A worked example
Let’s make this concrete. Suppose you’re adding a password reset flow to an existing web app. Here’s the difference between a prompt and a spec.
The prompt version:
“Add a password reset feature to my app. Users should be able to reset their password by email.”
An agent will build a password reset. It’ll invent token lifetimes, decide on its own whether to reveal if an email exists, pick an email-sending approach, and probably skip rate limiting entirely. You’ll discover the gaps in code review, or in production.
The spec version reads like a design doc, and every ambiguous decision the prompt left to chance is now nailed down:
{
Spec: Password Reset via Email
Objective
Let registered users who forgot their password regain access securely,
without support intervention. Why: password-related tickets are our #1
support cost, and the current “email support” flow takes ~18 hours.
Scope
– Request a reset link by email
– Reset the password using a one-time, time-limited token
– Invalidate the token after use
Non-goals (do NOT build)
– Account lockout / brute-force banning (separate spec, handled by
existing RateLimitService)
– SMS or authenticator-based recovery
– Changing the existing login flow
– Admin-initiated password resets
Context and constraints
– Stack: Node.js + Express, PostgreSQL via Prisma, React frontend
– Reuse existing EmailService.send(to, template, vars). Do NOT add
a new mail dependency
– Follow the existing error-handling pattern in src/lib/errors.ts
(throw typed AppError, never leak stack traces)
– Password hashing: reuse hashPassword() from src/lib/auth.ts
(argon2id). Never write a new hasher
Behavior
- Request reset -> POST /api/auth/reset-request
– Body: { email: string }
– Always returns 200 with { ok: true }, regardless of whether the
email exists (prevents account enumeration)
– If the email maps to a real user, generate a token and email a link:
${APP_URL}/reset?token=<token>
- Submit new password -> POST /api/auth/reset-confirm
– Body: { token: string, newPassword: string }
– Success: 200 { ok: true }
– Invalid/expired/used token: 400 { error: “invalid_token” }
– Weak password (< 12 chars): 400 { error: “weak_password” }
Data model (new table)
password_reset_tokens
id uuid pk
user_id uuid fk -> users.id
token_hash text (store a SHA-256 hash, NEVER the raw token)
expires_at timestamptz
used_at timestamptz nullable
created_at timestamptz default now()
Token rules
– 32 bytes of CSPRNG randomness, base64url-encoded
– Store only the hash; the raw token lives only in the emailed link
– Expires 30 minutes after creation
– Single use: set used_at on success; reject if used_at is not null
Edge cases
– Multiple requests: latest token valid; older unused tokens for the
same user are invalidated on each new request
– Reset with a valid token but the user was deleted meanwhile -> 400
invalid_token
– On successful reset, invalidate all of that user’s active sessions
Acceptance criteria (write tests for each)
– reset-request returns 200 for both real and unknown emails
– a valid token resets the password and can’t be reused
– an expired token (>30 min) is rejected
– requesting a new token invalidates the previous one
– successful reset logs the user out of all existing sessions
– raw tokens never appear in the database or logs
Boundaries
– Always: run npm test before finishing; follow existing lint rules
– Ask first: any change to the users table; adding npm packages
– Never: log tokens or passwords; weaken the enumeration protection
}
Why this is worth eight minutes
That’s maybe eight minutes of writing. In return, you’ve eliminated a dozen decisions the agent would otherwise have guessed at, including the enumeration defense, token hashing, expiry, single-use, and session invalidation, each of which is a real security bug when guessed wrong. The spec is also now a permanent artifact: reviewable in a PR, reusable as a template for the next auth feature, and readable by the next engineer (or agent) who touches this code.
Crucially, the spec is model-agnostic. Hand it to any capable coding agent and you’ll get an implementation that satisfies the same contract. You didn’t write a prompt. You wrote a specification.
The spec-driven workflow
Most modern tooling, including GitHub’s open-source Spec Kit, Amazon’s Kiro IDE, and the DIY setups people build on top of Cursor or Claude Code, converges on the same four-phase loop, with a validation checkpoint between each phase:
- Specify. Describe what and why at a high level; the agent drafts a detailed spec. You review and correct it. This is where you catch hallucinations before they become code.
- Plan. The agent turns the approved spec into a technical plan: architecture, data model, integration points.
- Tasks. The plan is broken into small, atomic, independently verifiable units of work.
- Implement. The agent writes code and tests against each task, and you verify against the spec.
The non-negotiable rule: don’t advance to the next phase until the current one is validated. The whole point is to front-load the thinking so the agent works inside guardrails instead of freelancing.
And the spec is a living artifact. When requirements change or you cut a feature, you update the spec first, then let it drive the code, the same way you’d update version-controlled documentation. This is what people mean by “intent is the new source of truth.” In the most aggressive form, spec-as-source, a change to the spec regenerates the affected code and no human edits the implementation directly. Most teams sensibly sit in the middle, in a spec-anchored mode where the spec and the tests co-evolve with the code and stay synchronized through CI.
Anti-patterns to avoid
Spec engineering has its own failure modes, and they’re worth naming.
The vague spec. “Build me something cool” or “make it better” gives the agent nothing to anchor on. Analysis of thousands of real agent instruction files found the number one reason they fail is that they’re simply too vague. Specificity is the whole job.
Spec bloat. The opposite failure. Teams go overboard, document everything, and drown the agent’s context window until it starts choking on volume. Specs need an information architecture. Split them, don’t pile everything into one monster file, and let the tooling pull in only what’s relevant.
Over-specifying too early. For a small script or a one-line UI tweak, a spec is overhead with no payoff. A sharp prompt is the right tool there. Reach for a spec when there’s genuine ambiguity, multiple stakeholders, or a contract others depend on.
The write-once spec. A spec you write and abandon rots immediately and starts lying to everyone who reads it. If it isn’t maintained, it’s worse than no spec.
Forgetting it’s still probabilistic. The same spec handed to two agents, or the same agent twice, won’t produce byte-identical code. Specs reduce ambiguity dramatically, but they aren’t compilers. Your acceptance tests are what make the outcome trustworthy.
Where this is going
A few threads are worth watching. Specs as code is becoming standard: versioned alongside the source, diffed in pull requests, with traceability links between a task, the commit that implemented it, and the tests that verify it. Spec evals are the obvious next frontier. Just as we built test-coverage and code-quality metrics, we’ll need ways to score how complete, testable, and maintainable a spec is. Those metrics mostly don’t exist yet. And security-by-construction approaches are emerging that bake non-negotiable constraints into the spec layer, so AI-generated code is safe by design rather than by after-the-fact review.
There are honest open problems, too. Specs are still somewhat model-sensitive, so what lands perfectly with one model may need tuning for another, and portability across tools isn’t solved. We’re roughly where cloud-native was before its standards settled. That’s not a reason to wait. It’s a reason to start in plain text and iterate on what your agents actually respond to.
The bottom line
Prompt engineering was the first visible way developers talked to AI, and it was genuinely useful. But it was never the destination. The durable skill, the one that compounds, survives model upgrades, and scales past toy problems, is the ability to express intent clearly, structurally, and reproducibly.
Great engineers aren’t becoming prompt whisperers. They’re becoming specification designers. The good news is that this isn’t a new superpower you have to learn from nothing. It’s the old discipline of writing a clear design doc, pointed at a new and extraordinarily fast collaborator. Write the spec. Let the agent build. Verify against the contract. Repeat.
The recipe beats the wish every time.