Spec-Driven Engineering: How to Write Specs That Agents (and Humans) Can Build From
In my post on agentic engineering, I argued that the bottleneck has shifted from writing code to writing specs. When an AI agent can implement a well-specified feature end-to-end, the spec becomes the most important artifact in the development process.
But what does a good spec actually look like? After writing dozens of them for production systems, I've developed a set of practices that consistently produce specs engineers (and agents) can build from directly, without follow-up Slack threads or ambiguity.
Throughout this post, I'll use a notification engine as the running example: a multi-channel, multi-tenant system that handles transactional alerts, marketing digests, and quiet hours. It's complex enough to exercise every practice, and generic enough that the patterns translate to whatever you're building.
Here's the framework.
In this post:
- Start with the overview table: the TL;DR contract
- Enumerate every input: source, fields, optionality
- Use ASCII wireframes: version-controlled, structure-focused
- Define parallel API contracts: both languages, field-aligned
- Visualize the pipeline: diagram for shape, detail for depth
- Include data examples: real values, not type signatures
- Map gaps to existing systems: build vs. reuse
- Spec every variant: no "general case" hand-waving
- Include a phase plan: independently shippable milestones
- List what you're NOT building: scope is defined by exclusions
1. Start with the overview table
Every spec should open with a dense table that gives readers the key parameters at a glance. An engineer should be able to read this table and know whether the spec is relevant to them, what scale they're designing for, and what the output looks like.
| Attribute | Value |
|---|---|
| Feature | Order Notification System |
| Channels | Email, SMS, Push, In-App |
| Volume | ~200K notifications/day |
| Latency Target | Under 30 seconds for transactional |
| Batching | Digest every 4 hours for marketing |
| Tenancy | Multi-tenant, per-tenant templates |
| Dependencies | Event bus, user preferences, email provider |
This is the "TL;DR contract" of the spec. It prevents people from reading three pages before understanding what they're building. Include volume, latency targets, channels, and key dependencies: the constraints that shape architecture decisions.
2. Enumerate every input
One of the most common spec failures is hand-waving over inputs. "The system takes order data" tells engineers nothing. Break inputs into a structured table with four columns: what the data is, what fields it contains, where it comes from, and whether it's required.
| Input | Fields | Source | Required |
|---|---|---|---|
| Order event | order_id, status, items, total | Event bus | Required |
| Customer prefs | channels[], quiet_hours, lang | Preferences API | Required |
| Tenant config | templates, branding, sender | Config service | Required |
| Delivery address | city, state, postal_code | Order service | Optional |
| Product images | thumbnail_url, alt_text | Catalog service | Optional |
The Source column forces you to think about where the data actually lives and whether that system exists yet. If the source says "Catalog service" and you don't have one, you've just identified a dependency. The Required/Optional column draws a clear line between MVP and enhancements.
3. Use ASCII wireframes for UI flows
ASCII wireframes are superior to image mockups in specs for three reasons:
- They live in version control and diff cleanly
- They force you to focus on structure over aesthetics
- They capture layout, hierarchy, data, and interactions in one artifact
+---------------------------------------------------------------+
| <- Back to Settings [ Save ] |
|---------------------------------------------------------------|
| |
| Notification Preferences |
| |
| +-------------------------------------------------------+ |
| | Channels [ pen ]| |
| | --------------------------------------------------- | |
| | Email: [x] Transactional [x] Marketing | |
| | SMS: [x] Transactional [ ] Marketing | |
| | Push: [x] Transactional [x] Marketing | |
| +-------------------------------------------------------+ |
| |
| +-------------------------------------------------------+ |
| | Quiet Hours [ pen ]| |
| | --------------------------------------------------- | |
| | Start: 10:00 PM | |
| | End: 8:00 AM | |
| | Timezone: IST (auto-detected) | |
| +-------------------------------------------------------+ |
| |
| [ Cancel ] [ Save Changes ] |
+---------------------------------------------------------------+
Notice how the wireframe captures navigation (Back, Save), section structure (cards with edit icons), actual data labels with example values, and CTA positioning. This gives an engineer enough to build the skeleton without a Figma link.
4. Define parallel API contracts
When frontend and backend are developed in parallel, the spec is the contract. Define request and response schemas in both languages, with field-level alignment.
# Backend (Python/Pydantic)
class NotificationPrefsUpdate(BaseModel):
channels: ChannelConfig
quiet_hours: QuietHours | None = None
digest_frequency: str = "4h"
// Frontend (TypeScript)
interface NotificationPrefsUpdate {
channels: ChannelConfig;
quietHours: QuietHours | null;
digestFrequency: string;
}
Showing both sides forces you to handle the snake_case/camelCase translation explicitly and catches field mismatches before they become runtime bugs.
Add an error codes table for every endpoint. This is the contract for how failures are communicated:
| Code | HTTP | Description |
|---|---|---|
| INVALID_CHANNEL | 400 | Unsupported notification channel |
| QUIET_HOURS_OVERLAP | 422 | Start time is after end time |
| TENANT_TEMPLATE_MISSING | 404 | No template configured for tenant |
Without this, frontend and backend will invent different error handling patterns.
5. Visualize the pipeline as stages
For any multi-step process, break it into numbered stages. Each stage documents its inputs, processing, and outputs.
Below the diagram, expand each stage with pseudo-code:
# Stage 2: Transform
# Inputs: validated event, customer prefs, tenant config
# Processing:
# 1. Select template based on event type + tenant
# 2. Render template with order data
# 3. Determine delivery channels from prefs
# 4. Apply quiet hours (defer if within window)
# 5. Check digest eligibility (batch if marketing)
# Output: list of DeliveryTask(channel, recipient, content, send_at)
This pattern lets readers zoom in or out depending on what they need. Product managers read the diagram. Engineers read the pseudo-code. Both get what they need from the same spec.
6. Include data examples, not just schemas
Schemas tell you the shape. Examples tell you the content. Always include at least one fully populated JSON example alongside your schema definitions.
{
"notification_id": "ntf_8x2kp9",
"event_type": "order_shipped",
"recipient": {
"user_id": "usr_4f2a",
"email": "[email protected]",
"phone": "+91-98765-43210",
"preferred_language": "en"
},
"content": {
"subject": "Your order #ORD-1234 has shipped",
"body": "Expected delivery: April 15, 2026",
"cta_url": "https://track.example.com/ORD-1234"
},
"delivery": {
"channels": ["email", "push"],
"send_at": "2026-04-13T14:30:00Z",
"digest_eligible": false
}
}
The example does things the schema cannot:
- It shows realistic values (not just "string")
- It reveals the actual shape of nested objects
- It acts as test data for engineers building the feature
When you see "channels": ["email", "push"] you immediately know it's a multi-select, not a single choice. The schema channels: string[] doesn't tell you that.
7. Map gaps to existing systems
Every spec introduces new work. A gap analysis table makes explicit what already exists, what partially exists, and what needs to be built from scratch. This is the foundation for effort estimation.
| Component | Current State | Gap |
|---|---|---|
| Event bus | Exists (Kafka) | None |
| User preferences | Exists (Preferences API) | Add channel config |
| Email delivery | Exists (SendGrid) | None |
| SMS delivery | Not supported | Build + vendor select |
| Push notifications | Partial (Firebase) | Add template support |
| Digest batching | Not supported | Build from scratch |
| Template engine | Not supported | Build or buy |
This table prevents the "everything is new" illusion: in this example, half the infrastructure already exists. Without the mapping, the project looks twice as large as it actually is.
It also prevents the "this is easy" illusion by surfacing components marked "Not supported."
8. Spec every variant explicitly
When a feature has multiple modes, don't describe the "general case" and hope readers extrapolate. Spell out each variant with its specific behavior.
Transactional (e.g., order_shipped):
- Immediate delivery, all opted-in channels
- No batching, no quiet hours override
- Template: "Your order #{order_id} has shipped"
Marketing (e.g., weekly_picks):
- Digest-eligible, email + push only (no SMS)
- Respects quiet hours
- Template: "This week's picks for you"
- Includes unsubscribe link
System (e.g., password_reset):
- Immediate delivery, email only
- Bypasses quiet hours, no unsubscribe option
- 15-minute expiry on link
Notice how the system variant bypasses quiet hours and has no unsubscribe option, while marketing requires both. These structural differences are invisible in a generic spec but critical for implementation. Each variant is a distinct code path.
9. Include a phase plan
Specs should answer "what do we build first?" A phased plan prevents the feature from being built bottom-up (infrastructure first, user value last).
Phase 1: Core Flow (MVP)
- Event bus consumer for order events
- Email delivery via SendGrid
- Single default template per event type
- Basic preferences (opt-in/opt-out per channel)
Phase 2: Multi-Channel + Templates
- SMS delivery via Twilio
- Push notifications via Firebase
- Per-tenant template engine
- Quiet hours support
Phase 3: Digest + Intelligence
- Marketing digest batching (4-hour window)
- Channel optimization (best channel per user)
- Delivery analytics dashboard
Each phase should be independently shippable. Phase 1 delivers working email notifications. Phase 2 adds channels and customization. Phase 3 adds intelligence. This structure lets engineering and product negotiate scope at phase boundaries rather than mid-feature.
10. List what you're NOT building
Scope is defined as much by what's excluded as what's included. A "Not building" section prevents scope creep and prevents future readers from wondering why something was left out.
| Excluded | Why |
|---|---|
| In-app notification center | Separate initiative, different team |
| ML-based send time optimization | Phase 3+ after delivery data is available |
| WhatsApp channel | Vendor approval pending, revisit Q3 |
| Read receipts for SMS | No reliable API, not worth the complexity |
The "Why" column is essential. Without it, someone will re-propose the excluded item in a future sprint. With it, they can evaluate whether the original reasoning still holds.
The bottom line
A good spec eliminates ambiguity. The practices above share a single philosophy: be concrete, be structured, and make the implicit explicit.
| What you write | What it prevents |
|---|---|
| Overview table | Three pages before anyone understands the scope |
| Input enumeration | "Where does this data come from?" in Slack |
| ASCII wireframes | Figma links that break and can't be diffed |
| Dual-language contracts | snake_case/camelCase bugs at integration |
| Pipeline diagrams | "Wait, which service calls which?" |
| Data examples | Schemas that look right but produce wrong output |
| Gap analysis | "Everything is new" or "this is easy" illusions |
| Variant specs | Hidden code paths discovered during QA |
| Phase plans | Infrastructure-first builds that never ship value |
| Exclusion lists | Scope creep from well-meaning re-proposals |
A 2,000-word spec that requires 40 hours of Slack clarification is worse than a 10,000-word spec that engineers can build from directly. Write the spec you wish you had when you joined the project.
And when an AI agent is the one building from the spec, the bar is even higher. Agents don't ask for clarification. They make assumptions. Every ambiguity in the spec becomes a coin flip in the implementation. These practices don't just make specs better for humans. They make specs executable.
This post is a companion to Agentic Engineering with Claude Code, which covers the spec-first workflow in practice. For context engineering techniques that improve spec-writing conversations with AI, see Context Engineering > Prompt Engineering.
Related writing
Security Practices and Tools in the Age of LLMs
How to actually secure LLM and agentic applications in production. Why the model isn't your attack surface, the one rule that predicts agent breaches, and the practices and tools that hold up.
Testing Practices in the Age of Agents
A deep, code-first guide to testing LLM and agentic flows: contract tests, hermetic mocking, statistical gates, LLM-as-judge, multi-turn simulation, and the CI setup that ties it together.
How a Diffusion Model Works: A Practitioner's Read of the 2026 Image Stack
Modern image models aren't U-Nets running 50 denoising steps. They're transformers running 4 steps of a straight-line flow. Once that lands, every product surface starts making sense.