Direct Answer
An AI coding spec is implementation-ready when another engineer or coding agent can determine what must change, what must not change, and how to prove the result without guessing hidden business rules. The useful unit is not a long prompt. It is a versioned behavior contract whose requirements map to observable scenarios, technical decisions, reviewable tasks, and independent evidence.
This guide shows how to write that contract. The worked example is a Go order-cancellation API with ownership, idempotency, state-transition, concurrency, and rollback requirements. The example uses OpenSpec's proposal.md, delta spec.md, design.md, and tasks.md structure, but the method works with an RFC, an ADR-backed design, or plain Markdown.
Start With the Decision, Not the Document
A useful spec begins with the decision the team must make and the evidence needed to accept it. It does not begin with a template, an AI tool, or a list of screens.
Write these six statements before asking an agent to draft anything:
| Question | Required answer |
|---|---|
| Who has the problem? | A named actor or downstream system |
| What outcome changes? | One observable behavior |
| What is outside this change? | Explicit non-goals |
| Which contracts can break? | API, data, authorization, events, latency, compatibility |
| What can go wrong? | Invalid state, retries, concurrency, dependency failure, abuse |
| What proves completion? | Tests, measurements, review, migration, and rollback evidence |
If the answer is “the agent can infer it from the repository,” the spec still contains an assumption. The repository can show current mechanics; it cannot decide the intended product behavior.
The Spec Coding concept guide explains why specifications act as decision contracts. This page focuses on writing the artifacts themselves. For current OpenSpec installation, commands, profiles, delta semantics, and archive behavior, use the separate OpenSpec tutorial.
Separate Five Kinds of Information
Specifications become difficult to review when motivation, behavior, design, tasks, and evidence are mixed into one checklist. Keep each concern in the artifact that owns it.
| Layer | Question it answers | Good content | Content that does not belong |
|---|---|---|---|
| Proposal | Why this change, and why this scope? | Problem, outcome, affected capabilities, impact | Class names, task checkboxes |
| Behavior spec | What must observers see? | Inputs, outputs, state rules, errors, scenarios | Framework choice, implementation sequence |
| Design | How will the system satisfy the contract? | Architecture, alternatives, data consistency, migration, risks | Repeated business requirements |
| Tasks | In what dependency order will work land? | Small changes with a verification method | New decisions hidden inside a checkbox |
| Evidence | What proves the shipped result? | Test reports, benchmark manifests, review records, rollout state | “Agent says complete” |
OpenSpec's current default schema uses the same separation: proposal first, specifications and design next, then tasks. Its documentation also makes design.md conditional and allows specification-free changes only when the change truly has no behavior delta. That is a workflow convention, not a universal law; the durable principle is to keep why, what, how, work, and proof distinguishable.
Build the Spec From Observable Behavior
A requirement should describe behavior that a user, client, operator, or downstream system can observe. Internal choices belong in the design unless callers rely on them.
Compare these statements:
| Weak statement | Why it fails | Testable replacement |
|---|---|---|
| “Cancellation should be fast.” | No threshold, load, or observation point | “At 50 requests/s with the agreed fixture, API latency MUST remain below 250 ms at p95, measured at the gateway.” |
| “Use optimistic locking.” | Prescribes a mechanism before the behavior | “Two concurrent cancellation attempts MUST produce one state transition and one cancellation event.” |
| “Handle errors gracefully.” | No error taxonomy or caller contract | “An ineligible state MUST return 409 order_not_cancellable and MUST NOT change the order.” |
| “Only owners can cancel.” | Does not define information leakage | “A caller who does not own the order MUST receive the same 404 order_not_found response as an unknown order ID.” |
| “Support retries.” | Does not define replay semantics | “Repeating an accepted cancellation MUST return the cancelled representation without a second event.” |
The words MUST, SHOULD, and MAY can clarify requirement levels when the document explicitly adopts BCP 14, which combines RFC 2119 with RFC 8174's capitalization rule. Use them sparingly. Capital letters do not make an ambiguous statement measurable, and they do not justify a design preference.
Worked Example: Cancel an Order Safely
The example change adds POST /v1/orders/{id}/cancel to an existing service. Cancellation looks small, but it forces the spec to resolve identity, state, retries, concurrent requests, events, and backward compatibility.
Write proposal.md as a Scope Contract
The proposal should be short enough to review before implementation. It establishes the problem and the boundary.
# Proposal: add-order-cancellation
## Why
Customers currently need support staff to cancel an unfulfilled order.
The service needs a self-service cancellation path with explicit state
and authorization behavior.
## What Changes
- Add an authenticated cancellation operation for order owners.
- Make successful retries idempotent.
- Emit one cancellation event for one state transition.
- Preserve the existing order-read response.
## Non-Goals
- Refund settled payments.
- Cancel fulfilled or shipped orders.
- Add administrator cancellation.
- Redesign the order state machine.
## Impact
- HTTP API, order repository, event outbox, audit log, and client SDK.
The non-goals prevent plausible but unauthorized expansion. They also tell reviewers when a newly discovered dependency requires a spec revision instead of an improvised implementation.
Write spec.md as a Behavior Contract
Each requirement should be singular, named, and paired with scenarios. Given/When/Then is useful because it separates initial state, event, and observable outcome. The Cucumber Gherkin reference also warns that a Then should verify an observable result rather than a hidden implementation detail.
## ADDED Requirements
### Requirement: An owner can cancel an eligible order
The service SHALL allow the authenticated owner to cancel an order
whose current status is PENDING.
#### Scenario: First accepted cancellation
- **GIVEN** order `o-42` belongs to caller `u-7` and is PENDING
- **WHEN** `u-7` requests cancellation of `o-42`
- **THEN** the response status is 200
- **AND** the response status field is CANCELLED
- **AND** exactly one cancellation event becomes observable
#### Scenario: Accepted cancellation is retried
- **GIVEN** order `o-42` is already CANCELLED by an accepted request
- **WHEN** owner `u-7` repeats the cancellation
- **THEN** the response status is 200 with status CANCELLED
- **AND** no additional cancellation event is emitted
### Requirement: Cancellation does not disclose another user's order
The service SHALL NOT reveal whether an order belongs to another user.
#### Scenario: Caller is not the owner
- **GIVEN** order `o-42` belongs to `u-7`
- **WHEN** caller `u-9` requests cancellation of `o-42`
- **THEN** the response is `404 order_not_found`
- **AND** the order and event stream remain unchanged
### Requirement: Ineligible states are rejected
The service SHALL reject cancellation after fulfillment begins.
#### Scenario: Order is fulfilled
- **GIVEN** order `o-42` is FULFILLED
- **WHEN** its owner requests cancellation
- **THEN** the response is `409 order_not_cancellable`
- **AND** the order and event stream remain unchanged
This is not automatically an executable test. Gherkin syntax becomes executable only when steps are connected to deterministic checks. Even without Cucumber, the structure helps reviewers see missing states and lets implementation tests use the same scenario IDs.
Add an Explicit Interface Contract
Prose cannot replace a schema. For an HTTP change, define the method, path, authentication, request shape, success response, error body, compatibility policy, and idempotency semantics in OpenAPI or another machine-readable interface.
| Contract item | Decision |
|---|---|
| Operation | POST /v1/orders/{orderId}/cancel |
| Authentication | Existing bearer-token middleware |
| Success | 200 with current order ID, status, and version |
| Unknown or foreign order | 404 with stable code order_not_found |
| Ineligible state | 409 with stable code order_not_cancellable |
| Retry | Already-cancelled order returns 200; no duplicate event |
| Concurrency | Conditional state update wins once; losers read the accepted state |
| Compatibility | Existing fields and order-read behavior remain unchanged |
The OpenAPI Specification defines a language-agnostic interface description so humans and software can understand an HTTP service without inspecting its source. The spec should reference the exact checked-in API file and validation command rather than copying a second, drifting schema into prose.
Put Mechanisms and Trade-offs in design.md
The design explains how the implementation will uphold the behavior:
# Design
## Decisions
1. The repository performs an atomic `PENDING -> CANCELLED` transition
with the current version as a compare-and-swap condition.
2. The cancellation event is written to an outbox in the same
transaction as the state transition.
3. Unknown and foreign-owned orders share one external error response;
internal audit records retain the distinction.
4. A retry reads the cancelled state and returns it without appending a
second event.
## Alternatives
- A distributed lock was rejected because the database transition can
enforce the required invariant with a smaller failure surface.
- Publishing directly to the broker was rejected because a commit can
succeed while publication fails.
## Risks and Mitigations
- Hot-order contention -> bounded retries and a concurrency test.
- Outbox backlog -> age and delivery-lag alerts.
- Client retry storm -> gateway rate limit and idempotent response.
## Migration and Rollback
- Add compatible storage fields before enabling the endpoint.
- Release behind a tenant allowlist.
- Disable the route without reverting accepted cancellation records.
“Use optimistic locking” is now justified by a required invariant. Another design may be valid if it produces the same externally observable behavior and operational evidence.
Make Quality Constraints Measurable
Non-functional requirements need a measurement contract. A naked threshold is not reproducible.
| Dimension | Incomplete | Reviewable |
|---|---|---|
| Performance | “p95 under 250 ms” | Load shape, fixture, warm-up, concurrency, observation boundary, percentile method, sample count |
| Reliability | “No duplicate events” | Event identity, retry schedule, crash points, observation window |
| Security | “Secure endpoint” | Actor matrix, denied cases, response equivalence, audit expectations |
| Compatibility | “No breaking changes” | Compared schema, clients, fixtures, and compatibility checker |
| Observability | “Add metrics” | Metric name, unit, labels, owner, alert condition, and cardinality limit |
| Rollback | “Can roll back” | Trigger, command or flag, state compatibility, owner, and proof |
Do not invent a universal latency target. A threshold is meaningful only with the system's SLO and a representative workload. Likewise, “one event” requires an event identity and an observation window, not only a unit-test mock.
Security criteria should be part of the change, not a final generic checklist. NIST SSDF 1.1 organizes secure development around preparing the organization, protecting software, producing well-secured software, and responding to vulnerabilities. A feature spec need not reproduce the framework, but it should map relevant security requirements to concrete controls and evidence.
Turn Scenarios Into Runnable Go Evidence
The domain function below implements only the in-memory decision boundary. It does not claim to prove database atomicity, HTTP response mapping, outbox delivery, or authorization middleware. Those require repository, handler, and integration tests.
package cancellation
import "errors"
type Status string
const (
StatusPending Status = "PENDING"
StatusCancelled Status = "CANCELLED"
StatusFulfilled Status = "FULFILLED"
)
var (
ErrUnauthenticated = errors.New("unauthenticated")
ErrNotFound = errors.New("order_not_found")
ErrNotCancellable = errors.New("order_not_cancellable")
)
type Order struct {
ID string
OwnerID string
Status Status
Version int
}
type Result struct {
Order Order
Changed bool
}
func Cancel(order Order, actorID string) (Result, error) {
if actorID == "" {
return Result{}, ErrUnauthenticated
}
if actorID != order.OwnerID {
return Result{}, ErrNotFound
}
switch order.Status {
case StatusCancelled:
return Result{Order: order, Changed: false}, nil
case StatusPending:
order.Status = StatusCancelled
order.Version++
return Result{Order: order, Changed: true}, nil
default:
return Result{}, ErrNotCancellable
}
}
Use table-driven tests so each case points back to one scenario. A passing test proves only the behavior inside this boundary; the evidence matrix below prevents that narrow result from being mistaken for end-to-end completion.
package cancellation
import (
"errors"
"testing"
)
func TestCancel(t *testing.T) {
tests := []struct {
name string
order Order
actorID string
wantStatus Status
wantVersion int
wantChanged bool
wantErr error
}{
{
name: "AC-1 owner cancels pending order",
order: Order{ID: "o-42", OwnerID: "u-7", Status: StatusPending, Version: 3},
actorID: "u-7",
wantStatus: StatusCancelled,
wantVersion: 4,
wantChanged: true,
},
{
name: "AC-2 retry returns accepted state",
order: Order{ID: "o-42", OwnerID: "u-7", Status: StatusCancelled, Version: 4},
actorID: "u-7",
wantStatus: StatusCancelled,
wantVersion: 4,
wantChanged: false,
},
{
name: "AC-3 foreign caller cannot discover order",
order: Order{ID: "o-42", OwnerID: "u-7", Status: StatusPending, Version: 3},
actorID: "u-9",
wantErr: ErrNotFound,
},
{
name: "AC-4 fulfilled order is ineligible",
order: Order{ID: "o-42", OwnerID: "u-7", Status: StatusFulfilled, Version: 3},
actorID: "u-7",
wantErr: ErrNotCancellable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Cancel(tt.order, tt.actorID)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("error = %v, want %v", err, tt.wantErr)
}
if tt.wantErr != nil {
return
}
if got.Order.Status != tt.wantStatus ||
got.Order.Version != tt.wantVersion ||
got.Changed != tt.wantChanged {
t.Fatalf("result = %+v", got)
}
})
}
}
Expected result:
ok example.com/specdemo/cancellation
Build a Requirement-to-Evidence Matrix
Traceability stops “all tests passed” from hiding untested requirements. Give each requirement and scenario a stable ID, then name the evidence type and owner.
| Requirement | Scenario | Evidence | Gate |
|---|---|---|---|
ORD-CANCEL-1 |
Owner cancels pending order | Domain test + HTTP contract test | Required in CI |
ORD-CANCEL-1 |
Retry is idempotent | Handler test + event-count assertion | Required in CI |
ORD-CANCEL-2 |
Foreign order is hidden | Authorization integration test | Required in CI |
ORD-CANCEL-3 |
Fulfilled order is rejected | Domain and API tests | Required in CI |
ORD-CANCEL-4 |
Two concurrent requests | Database race test + outbox assertion | Required before canary |
ORD-CANCEL-5 |
p95 latency budget | Versioned load-test report | Required before expansion |
ORD-CANCEL-6 |
Route can be disabled | Staging rollback drill | Required before canary |
The matrix should expose missing evidence rather than silently treating it as success. A unit test cannot prove a database race, a schema linter cannot prove authorization, and an AI review cannot replace an independently executed test.
For higher-risk agent workflows, the same principle appears in agent harness evaluation: define the trajectory, permissions, outcomes, and evidence before trusting a completion claim.
Write Tasks as Verifiable Delivery Slices
Tasks should follow dependencies and land their own tests. Avoid layer-by-layer plans such as “build all models, then handlers, then tests,” because they postpone evidence until the end.
## 1. Domain transition
- [ ] 1.1 Implement PENDING -> CANCELLED and idempotent retry;
verify with AC-1, AC-2, and AC-4 table tests.
## 2. Persistence and events
- [ ] 2.1 Add atomic conditional update and transactional outbox write;
verify AC-4 with two concurrent requests and one event ID.
## 3. HTTP contract and authorization
- [ ] 3.1 Add the OpenAPI operation and generated contract fixtures;
verify success, stable error codes, and compatibility.
- [ ] 3.2 Connect existing authentication and ownership checks;
verify AC-3 without exposing order existence.
## 4. Release
- [ ] 4.1 Add metrics, alert ownership, and tenant allowlist;
verify the staging dashboard and rollback drill.
A checkbox is progress state, not proof. The task text should name the artifact or command that a reviewer can inspect.
Use a Review Gate Before Implementation
Ask the agent to critique the spec before it writes code. The review should produce questions and contradictions, not silently “improve” requirements.
Use this gate:
- Identity: Does every actor, resource, and external dependency have a stable name?
- States: Are legal transitions and terminal states explicit?
- Errors: Does every invalid input, denied action, timeout, and dependency failure have observable behavior?
- Concurrency: What happens when requests race, repeat, arrive late, or resume after a crash?
- Data: Which fields are stored, logged, emitted, retained, redacted, or deleted?
- Compatibility: Which existing clients, schemas, events, and migrations must continue to work?
- Measurement: Where is each latency, quality, or reliability threshold observed?
- Verification: Can every
MUSTbe traced to a test, analysis, demonstration, or inspection? - Rollback: Can the change be disabled or reverted without corrupting accepted state?
- Open questions: Would any unanswered question change behavior, design, or task order? If yes, stop and resolve it.
The agent may help discover omissions, but the owner of the product and system must decide them. A fluent draft is not an approved requirement.
Control Change During Implementation
Specifications become stale when chat decisions bypass version control. Use the same change discipline for requirements that you use for code:
- Update the behavior contract before implementing changed behavior.
- Review the spec diff and code diff together.
- Record a deliberate deviation with its reason, owner, and expiry.
- Re-run every affected scenario, not only the new happy path.
- Archive or merge the accepted delta only after implementation evidence exists.
- Keep runtime configuration and emergency controls traceable to the release.
In OpenSpec, active changes hold delta specifications while the main specifications describe accepted current behavior. The current spec-driven schema distinguishes ADDED, MODIFIED, REMOVED, and RENAMED requirements, and a modified requirement must include its complete updated content. The OpenSpec schema reference is the authority for those product-specific mechanics.
Common Failure Modes
The spec repeats the ticket
“Add order cancellation” names a feature but does not define eligible states, callers, errors, retries, or proof. Add the decisions that an implementer would otherwise guess.
The spec dictates code but omits behavior
“Create CancellationService with a mutex” can still produce the wrong user result. Move replaceable mechanisms to design and state the invariant in the spec.
Happy-path scenarios masquerade as coverage
One successful example does not cover authorization, empty state, duplicate requests, concurrency, dependency failure, compatibility, or rollback. Build scenarios from the state and failure model.
Generated tests restate generated code
An agent can implement and test the same wrong assumption. Review expected outcomes against the requirement, use independent fixtures, and retain human review for security and business-critical logic. The AI code review pipeline guide explains how to separate generation from review evidence.
Thresholds have no workload
“p95 < 250 ms” is not reproducible without a request distribution, concurrency, environment, warm-up, sample size, and measurement boundary.
Tasks contain hidden product decisions
If a task says “add admin override” but no requirement authorizes it, implementation has escaped the approved scope. Revise the spec or remove the task.
The document is too large to challenge
Split by independently reviewable behavior, not by arbitrary page count. A small change may need one requirement and four scenarios. A cross-service migration may need several capability specs and a detailed design.
When a Full Spec Is Not Worth the Cost
Specification depth should follow risk and uncertainty.
| Change | Minimum useful artifact |
|---|---|
| Typo or mechanical rename | Task, diff, and targeted check |
| Local refactor with unchanged behavior | Scope, invariants, tests, and no-behavior-change assertion |
| User-visible feature | Behavior requirements, scenarios, interface contract, rollout |
| Data migration | Source/target schema, invariants, reconciliation, rollback |
| Authentication or authorization | Actor matrix, denied cases, audit, threat review, rollback |
| Cross-service protocol | Versioning, compatibility, failure semantics, observability |
| Irreversible or regulated action | Approval, evidence retention, recovery, and independent review |
The purpose is not maximum documentation. It is the smallest contract that makes consequential assumptions visible and testable.
Frequently Asked Questions
What should an AI coding spec contain?
Include the user-visible outcome, scope and non-goals, input/output and state contracts, authorization and failure behavior, measurable quality constraints, acceptance scenarios, rollout and rollback conditions, and a requirement-to-evidence map. Add design details only when they affect implementation safety or review.
How detailed should a coding spec be?
Specify every decision whose omission could change behavior, compatibility, security, data integrity, reliability, or acceptance. Do not prescribe replaceable implementation details. If two competent implementers can choose different internals and still satisfy the same scenarios, those internals usually belong in design.
Are Given, When, Then scenarios executable tests?
Not by themselves. They are structured examples. They become executable when mapped to step definitions, table tests, contract tests, or another deterministic verifier. Keep Then focused on observable outcomes so the test does not freeze an internal implementation unnecessarily.
What is the difference between a spec and a prompt?
A prompt initiates one model interaction. A spec persists as a reviewed, versioned contract across tools, sessions, commits, and releases. An implementation prompt should point to the approved spec and current task; it should not redefine required behavior in hidden chat history.
How do teams prevent spec drift?
Require behavior changes to update the spec before code, review both diffs together, use stable requirement IDs, trace each requirement to evidence, record temporary deviations, and reconcile the accepted contract before merge. Drift becomes visible when a test or implementation has no requirement, or a requirement has no evidence.
Sources and Related Reading
- ISO/IEC/IEEE 29148:2018 Requirements Engineering - requirements processes and information items; the published edition remains current while a revision is under development.
- BCP 14: RFC 2119 and RFC 8174 - precise meanings and capitalization rules for normative keywords.
- Cucumber Gherkin Reference - scenarios, Given/When/Then semantics, and observable outcomes.
- OpenAPI Specification - machine-readable HTTP interface contracts.
- NIST SP 800-218 SSDF 1.1 - secure software development practices and evidence boundaries.
- OpenSpec
spec-drivenSchema - current proposal, delta spec, design, and task artifact contracts. - Spec Coding Method
- OpenSpec Tutorial
- AI Code Review Pipeline