Skip to content

Writing Effective Specs

An agent-ready spec is written for a reader that takes everything literally, fills every gap with a statistically plausible guess, and never walks over to your desk to ask. That reader punishes vagueness harder than any human contractor — and rewards precision better, because it actually reads all of it, every time.

The completeness test

Before handing a spec to an agent, apply one test:

Could a competent contractor with no Slack access build this correctly?

No access to you, no tribal knowledge, no "obviously we meant business days." Just the spec and the repo. If the honest answer is "they'd have to ask about X" — the agent won't ask about X. It will decide X, silently, in clean confident code. Every question a contractor would ask is a decision the agent will invent. Find them by reading the spec adversarially: "what would I have to guess here?"

This test is why specs work across tools and time: the same document that constrains Claude Code today constrains Cursor tomorrow and a human next quarter.

Anatomy of an agent-ready spec

Ten sections, in the order agents benefit from reading them. Not every spec needs all ten — but for each one you skip, be able to say why. Fragments below are from one running example: spending alerts in a SaaS billing dashboard (notify account owners when monthly usage crosses configured thresholds). A fill-in-able version lives in Templates: Specs and Planning.

1. Problem and intent

Two paragraphs max: what hurts today, and what "good" looks like. This is the section that lets the agent make aligned micro-decisions — the hundreds of small calls no spec can enumerate get resolved against the stated intent instead of against training-data averages.

markdown
## Problem
Customers discover overage charges on their invoice, after the fact. Support gets
~40 tickets/month that are really "why didn't you warn me". Churn interviews cite
surprise bills as a top-3 reason.

## Intent
No customer should ever be surprised by an overage. Warn early, warn once per
threshold, never spam. This is a trust feature, not an engagement feature.

"Not an engagement feature" is doing real work: it steers dozens of downstream defaults (no daily digests, no marketing hooks, conservative send logic) without another word.

2. User stories — where relevant

Use stories only when there are distinct actors with distinct flows; skip them for infra and internal work. Their job in an agent spec is to bind requirements to a persona, so the agent doesn't design one UI for two conflicting users.

markdown
- As an account owner, I set thresholds once per workspace and get emailed when
  usage crosses them.
- As a member (non-owner), I can see current usage vs thresholds but cannot edit
  them or receive owner alerts.

3. Functional requirements

Numbered, individually testable statements. Numbering matters mechanically: it lets the agent's plan, the PR description, and your review all reference FR-3 instead of paraphrasing each other — and paraphrase is where drift starts.

markdown
- FR-1: Owners can configure 1–5 thresholds per workspace, as % of plan quota
  (25–100, integer).
- FR-2: When a metering run crosses a threshold, send exactly one email per
  threshold per billing period.
- FR-3: Crossing multiple thresholds in one run sends one combined email, not N.
- FR-4: Alert state resets at billing-period rollover.

4. API contracts

Concrete request/response pairs beat prose descriptions — agents copy shapes literally, so give them the exact shape. Include one error response; it forces the error-format decision now instead of at implementation time.

markdown
## API
PUT /api/workspaces/:id/alert-thresholds   (owner only)

Request:
    { "thresholds": [50, 80, 100] }

200:
    { "thresholds": [50, 80, 100], "updatedAt": "2026-07-10T09:14:00Z" }

422 (non-integer, out of range, >5 entries, duplicates):
    { "error": "invalid_thresholds", "detail": "values must be unique integers 25-100" }

5. Data model

Fields, types, nullability, constraints, indexes. Nullability and uniqueness are the decisions agents most often get subtly wrong — an agent that guesses "nullable" where you meant "required" produces a bug that surfaces weeks later. Say where the migration lives.

markdown
## Data model
alert_thresholds
  workspace_id   uuid  FK workspaces, NOT NULL
  percent        int   NOT NULL, CHECK 25 <= percent <= 100
  UNIQUE (workspace_id, percent)

alert_events    -- idempotency ledger, one row per sent alert
  workspace_id, percent, billing_period_start   UNIQUE together
  sent_at        timestamptz NOT NULL
Migration: additive only, in db/migrations/. No changes to existing tables.

6. Acceptance criteria

Checkable, not vibes. Each criterion should be verifiable by running something or looking at one specific place — if a criterion can't fail, it isn't one. These become the agent's definition of done and your review script; the best ones map one-to-one onto tests (Verification Strategy).

markdown
## Acceptance criteria
- [ ] Crossing 80% then re-running metering sends zero additional emails
      (verified by test on alert_events uniqueness).
- [ ] A run that jumps 40% -> 95% sends ONE email listing both 50% and 80%.
- [ ] Non-owner PUT to alert-thresholds returns 403, body matches error contract.
- [ ] Period rollover: same threshold can fire again in the new period.

Weak criterion for contrast: - [ ] Alerts work correctly — the agent will check that box with full confidence.

7. Edge cases, enumerated

The agent handles every edge case you list and guesses at every one you don't. Enumerate the ugly ones explicitly, including the resolution — an edge case without a decision is just anxiety.

markdown
## Edge cases
- Plan downgrade mid-period pushes usage above 100%: fire the 100% alert once,
  do not re-fire lower thresholds.
- Workspace has no owner (orphaned): skip silently, log at WARN.
- Email provider 5xx: retry per existing mailer policy; a lost alert is
  acceptable, a duplicate alert is not.
- Thresholds edited mid-period: already-sent alerts stay sent; new thresholds
  are eligible immediately.

Note the last email bullet encodes a priority (duplicates worse than drops). Agents are bad at inferring your failure preferences; state them.

8. Constraints

Performance, security, and compatibility bounds — with numbers. "Should be fast" constrains nothing; "adds ≤ 50ms p95 to the metering run" is a testable wall. Include the negative constraints: what the agent must not touch.

markdown
## Constraints
- Alert evaluation runs inside the existing metering job; adds <= 50ms p95.
- No new external dependencies; use the existing mailer (src/lib/mailer.ts).
- Threshold writes are owner-only, enforced server-side (not just hidden in UI).
- Do not modify the metering pipeline itself (src/metering/**) — read its output.

9. Out of scope

The single highest-value section for agents. Agents pattern-match toward the "complete" version of a feature seen in training data — notifications grow preference centers, exports grow schedulers, tables grow bulk actions. Requirements only bound what to build; nothing else in the spec bounds what to stop building. Out-of-scope is that brake, and it's also your cheapest scope-control during review: any diff touching a listed item is rejected without discussion. See Scope and Context Failures.

markdown
## Out of scope (do not build)
- In-app or Slack notification channels (email only in v1)
- Per-user notification preferences or unsubscribe (owners always get alerts)
- Absolute-dollar thresholds (percent-of-quota only)
- Any UI beyond the thresholds card on the existing Billing settings page
- Backfilling alerts for the current billing period at launch

Write it by asking: "what would an eager engineer add here unprompted?" Then ban it, item by item.

10. Migration notes

Only when existing data or behavior changes — but then, non-negotiable. Cover: existing-data handling, ordering (schema before code before backfill), rollback, and compatibility windows. For anything multi-step, the sequencing belongs in the spec, not in the agent's head. See Migration.

markdown
## Migration / rollout
1. Ship schema (additive). 2. Ship code behind flag `spending_alerts`.
3. Enable for 5 internal workspaces for one full billing period. 4. GA.
Rollback: disable flag. Tables are additive; no down-migration needed.
Existing workspaces start with zero thresholds (feature is opt-in) — no backfill.

Let the agent draft, then human-harden

Writing specs from a blank page is why people skip them. Don't. Have the agent produce the draft — it's good at structure, enumeration, and finding edge cases in existing code — then spend your 30 minutes hardening instead of typing. The critical clause is the interview instruction; without it the agent fills every gap with assumptions, which defeats the purpose:

text
Draft a spec for <feature> using the structure in docs/templates/spec.md.
Before writing anything, interview me: ask up to 8 questions about the decisions
you'd otherwise have to guess — do not resolve ambiguity yourself. After my
answers, write the draft. Mark every remaining decision you made unilaterally
with [ASSUMPTION] so I can accept or override each one. Read the existing code
in <relevant paths> first and flag any conflicts between my answers and current
behavior.

Human-hardening is where the value concentrates — the agent cannot do these parts:

  • Resolve every [ASSUMPTION] explicitly. Each one you leave standing is a requirement you delegated by accident.
  • Replace plausible numbers with real ones. Agents write "≤ 200ms" because specs in training data say that; your p95 budget is a fact, not a vibe.
  • Write out-of-scope yourself. The draft's exclusions will be generic; the real ones come from knowing your roadmap and your reviewers' fights.
  • Delete boilerplate. Agent drafts pad. A spec section that says nothing binding ("errors should be handled gracefully") is negative value — it trains you to skim.
  • Run the completeness test from the top of this page, adversarially.

Full drafting workflow, including the plan-mode feedback loop: Idea to Spec.

Spec granularity: one feature, one spec

A feature spec should cover roughly one PR chain — half a day to two days of agent work. Below that, a structured prompt suffices (see the decision table). Above that, don't write a bigger spec — decompose.

SignalYou have
Spec fits on ~1–2 pages, one coherent acceptance listA feature spec — proceed
Acceptance criteria cluster into groups that could ship separatelyAn epic wearing a spec costume — split it
Sections start with "Phase 1 / Phase 2"A decomposition doc plus N feature specs, pretending to be one file
The agent's plan for it exceeds ~10 steps or spans unrelated subsystemsContext-window burn and mid-task drift ahead — split before running

An epic gets a short decomposition doc — intent, ordered list of feature specs, and the dependency edges between them — and each child spec must be independently verifiable: its acceptance criteria checkable without the siblings shipped. That's what lets you run children as separate agent sessions (or parallel agents) without integration surprises. Sequencing and stitching: Large Tasks.

A field manual for AI-native software engineering.