Spec-Driven Development with OpenSpec: From Vague Request to Verifiable Change
Turn an idea into behavior you can review, implement, and test

“Let customers cancel orders” sounds like a small feature. A developer or an AI coding agent could start writing an endpoint immediately. A few hours later, the code might compile and the happy-path test might pass. We could still disagree about what happens after shipment starts, whether a retry creates a second cancellation, or which customer is allowed to act.
I use OpenSpec for this kind of conversation: make the intended behavior visible before implementation hides the unanswered questions. Spec-driven development (SDD) is useful when the expensive mistake is building a plausible interpretation of an unclear request. It does not remove judgment from the engineer. It gives people and AI agents something concrete to review, challenge, and verify.
Let's follow one illustrative change from the first request to a finished implementation. The business rules below are example decisions for this walkthrough, not universal rules for order systems.
The request is not yet a specification
Before opening an editor, ask what “cancel” means in this service:
| Question | A decision the team needs to make |
|---|---|
| Who may cancel? | The order owner, an administrator, or both? |
| Until when? | Only while PENDING, or also after payment and before shipment? |
| What does a retry do? | Return the existing cancellation, or fail? |
| What if two requests race? | Can both succeed without recording two transitions? |
| What happens downstream? | Is an OrderCancelled event part of the contract? |
The list is not an excuse to delay work indefinitely. It is a way to find the decisions that would otherwise be made accidentally in a controller, an ORM mapping, or an agent's generated code. For this example, assume the team agrees that the authenticated owner may cancel a PENDING order; a shipped order cannot be cancelled; repeating a successful request returns the cancelled state; and one logical OrderCancelled event is produced for the transition.
The goal is now specific enough to propose a change.
Create a change that explains why
With OpenSpec initialized in the project, create a named change:
openspec new change add-order-cancellation \
--goal "Customers can cancel their pending orders safely"
openspec status --change add-order-cancellation
The CLI creates openspec/changes/add-order-cancellation/. In OpenSpec's default spec-driven workflow, proposal.md captures why, a spec delta captures what behavior changes, design.md records how where a design is needed, and tasks.md tracks implementation. Proposal comes first; spec and design can follow in either order; tasks depend on both. The schema reference describes those roles.
The proposal should make the problem and scope clear without pretending to be the implementation plan. A shortened example:
# Proposal
## Why
Customers cannot cancel an eligible order without contacting support.
## What Changes
- Allow an authenticated owner to cancel a pending order.
- Preserve a single logical cancellation when requests are repeated.
- Publish the cancellation for downstream consumers.
## Capabilities
### New Capabilities
- `order-cancellation`: Rules and outcomes of cancelling an order.
## Impact
Order API, order persistence, cancellation event, and contract tests.
At review time, the most useful question is whether this is the right change. If the request also asks for refunds, inventory release, and cancelling shipments, the proposal needs to state which of those are in scope and which require another change. A small, named unit of work is easier to understand and integrate.
Put observable behavior in the spec
The delta spec belongs under the capability named in the proposal, for example openspec/changes/add-order-cancellation/specs/order-cancellation/spec.md. It describes the behavior that changes, not the classes or SQL statements that happen to implement it.
Here is an illustrative excerpt in OpenSpec's requirement-and-scenario format:
# Spec Delta
## Purpose
Define when the owner of an order may cancel it and how the service
responds to rejected or repeated cancellation requests.
## ADDED Requirements
### Requirement: Cancel an eligible order
The service MUST allow the authenticated owner to cancel a PENDING order.
#### Scenario: Eligible order
- **WHEN** the owner requests cancellation of a PENDING order
- **THEN** the order becomes CANCELLED and one logical OrderCancelled event is recorded
#### Scenario: Shipment has started
- **WHEN** the owner requests cancellation of a SHIPPED order
- **THEN** the service rejects the request and leaves the order unchanged
#### Scenario: Not the owner
- **WHEN** a different authenticated customer requests cancellation
- **THEN** the service rejects the request and leaves the order unchanged
### Requirement: Repeat a completed cancellation
The service MUST handle repeated requests without creating a second cancellation.
#### Scenario: Repeated request
- **WHEN** the owner repeats a successful cancellation request
- **THEN** the service returns the existing CANCELLED state without a new transition
This still leaves questions for the API contract: should a shipped order return 409 Conflict? What should an unauthenticated caller see? Those decisions should be made explicitly and added to the relevant spec or existing API contract before implementation. A spec is useful because its gaps are visible to a reviewer.
Keep the wording at the level of behavior. “Use optimistic locking in OrderRepository” is a possible design decision, not a customer-facing requirement. If the implementation can switch from one concurrency mechanism to another without changing the promise, the mechanism belongs in the design. OpenSpec's spec guidance makes the same distinction.
Use the design for technical trade-offs
The design connects those requirements to the actual system. For this example, it might record:
# Design
## Decisions
- Use an atomic conditional update to change PENDING to CANCELLED.
- Record the OrderCancelled event in the same database transaction.
- On a lost race, reload the order and return its current state when it is CANCELLED.
## Trade-offs
- A conditional update avoids holding a database lock across the request.
- The event must be durable even if the process stops after commit.
That is a sketch, not production-ready code. It shows where the technical reasoning lives. A database lock could be valid instead; the design should explain why the chosen approach fits this service. If the event is required for downstream consistency, the Transactional Outbox pattern is one way to make state and publication intent atomic.
Design is not compulsory paperwork for every change. OpenSpec can omit design.md when the change does not need a separate technical decision. It becomes valuable when the choice has consequences for concurrency, migrations, security, dependencies, or recovery.
Turn the plan into work you can verify
tasks.md turns the spec and design into an implementation checklist. Each item should end in an observable result:
- [ ] Add the cancellation API and authorization checks.
- [ ] Persist the order transition and event intent atomically.
- [ ] Test eligible, shipped, unauthorized, and repeated requests.
- [ ] Run the project checks and review the API response contract.
The spec says what must be true. The tasks say what work remains. Tests provide evidence that the implementation behaves as promised. None of these files replaces the others.
Before code starts, review the three questions OpenSpec's quickstart emphasizes: Is the proposal the right problem and size? Would you accept the spec's scenarios as done? Do the tasks cover those scenarios without adding unrelated work? Fixing a sentence now is cheaper than reviewing a large implementation of the wrong rule.
Give the agent a bounded handoff
Once the artifacts are agreed, the implementation request can be short:
Apply the add-order-cancellation change. Read its proposal, spec,
design, and tasks before editing code. Implement one task at a time.
Run the relevant tests and report any requirement that is ambiguous
or cannot be met by the current design. Do not invent a new business rule.
OpenSpec calls this the apply phase. It tracks progress in the checkboxes in tasks.md; it is not a promise that the CLI has turned the spec into correct code. The OpenSpec workflow explicitly allows the artifacts to be corrected while work is underway.
This is particularly useful with AI agents. A new session can read the change and the remaining tasks instead of relying on an old chat transcript. The agent still needs access to the actual codebase and its tests, and a person still needs to review behavior at the boundary of the change. If several agents work in parallel, give each a separate Git worktree and keep ownership of integration clear.
When implementation reveals a missing scenario
Suppose a test exposes a race: two cancellation requests read the same PENDING order before either writes. The original repeated-request scenario covers a request made after cancellation, but does not say what simultaneous requests should do.
Do not silently choose the easiest behavior in code. Add the missing scenario to the spec:
#### Scenario: Concurrent cancellation requests
- **WHEN** two requests from the owner try to cancel the same PENDING order
- **THEN** exactly one cancellation transition and one logical event are recorded
- **AND** both requests can observe the final CANCELLED state
Then revisit the design and tasks. The conditional update may let one request win and the other load the newly cancelled row; add a concurrency test that actually runs both requests. If the chosen API semantics require one request to report a conflict instead, record that decision in the spec and test it. The point is that the spec changes when understanding changes. It should describe the behavior the team intends to ship, not preserve an early guess for appearances.
Verify the implementation, then archive the change
The final review needs evidence for each important scenario:
| Scenario | Useful evidence |
|---|---|
| Eligible owner | API test confirms the state transition and response |
| Shipped order | API test confirms rejection and unchanged state |
| Different customer | API test confirms rejection and unchanged state |
| Repeated request | Test confirms no second logical transition or event |
| Concurrent requests | Integration test confirms the chosen race behavior |
| Downstream event | Test confirms the event intent commits with the order change |
Run OpenSpec's structural validation too:
openspec validate add-order-cancellation --strict
openspec status --change add-order-cancellation
Validation checks the change artifacts; it cannot prove that the service meets its requirements. Run the project's tests, inspect the diff, and compare the result with the spec. If code and spec disagree, decide which is wrong before declaring the task complete.
When the implementation and checklist are complete, archive the change. For a behavior-changing capability, OpenSpec moves the change folder to openspec/changes/archive/ and merges the delta requirements into the main specs, so they describe the system as built. The quickstart walks through this step. Use your team's normal Git review and integration process around it; OpenSpec does not replace that process.
Keep the method proportional to the change
SDD earns its keep when an unclear decision would be expensive: public API behavior, cross-service workflows, state transitions, migrations, or security rules. It is lighter when you keep each change small and write only the artifacts it needs. A typo fix does not require an invented business requirement. OpenSpec supports skip_specs: true for changes with no spec-level behavior change, such as documentation or some tooling work, and allows a design to be omitted when no design decision warrants it.
For me, the value is not producing more Markdown. It is discovering disagreement while the change is still easy to reshape, and giving implementation and review a shared definition of done. An AI agent can accelerate the code. A good spec makes it easier to tell whether that acceleration took us in the right direction.


