Writing Specs That Agents Can Act On
Learning Objectives
- write a complete spec for each of the four spec types
- apply the four-element framework to a new task from scratch
- construct a spec using the provided templates
- adapt an existing spec template to fit a different task context
Core Concepts
The four spec types form a chain from intent to verified output. Writing the right type for the task means the agent has the right structure of information for its role in that chain.
The Four Spec Types in Context
Each spec type answers a different question and targets a different stage of the work.
Product Spec answers: what needs to be built and why does it matter to users? A Product Spec for a rescheduling feature on the scheduling platform defines the user problem (clients need to change their booking without cancelling), the scope (reschedule via email link, single rebooking, no payment changes), and the high-level criteria that confirm the feature works (new slot confirmed, original slot released, both parties notified).
Functional Spec answers: how should the system behave, step by step and rule by rule? A Functional Spec for the client booking page defines the user flow from landing to confirmation, the system behaviour at each step (slot holds for 60 seconds during checkout), and the business rules the system must enforce (no double-booking, no past-date selection, confirmation email within 30 seconds).
Technical Spec answers: how will this be implemented, and what are the architectural decisions? A Technical Spec for the booking reservation endpoint defines the data model, the API surface, the concurrency approach (row-level lock on slot reservation), and the technology constraints (PostgreSQL, parameterised queries, service layer pattern).
Test Spec answers: what scenarios confirm the system behaves as specified? A Test Spec for the booking confirmation flow defines UI scenarios (client books last available slot, second client sees slot gone), API cases (POST /api/bookings returns 409 on conflict), integration checks (confirmation email delivered within 10 seconds), and validation rules (attendee count never exceeds slot capacity).
The Four-Element Framework
Every effective spec, regardless of type, contains four elements:
- Context: what situation or system does this spec operate in?
- Objective: what exactly needs to be produced or done?
- Constraints: what boundaries, rules, or non-negotiable requirements apply?
- Output format: what does the deliverable look like, and how will it be validated?
Omitting any element forces the agent to make assumptions. Those assumptions are usually wrong in ways that are expensive to fix.
Key Points
- Four spec types: Product, Functional, Technical, Test
- Each type answers a distinct question at a different stage of the chain: intent, behaviour, implementation, verification
- Every spec must contain context, objective, constraints, and output format
- The right spec type depends on the nature of the work, not personal preference
- A spec written for a human reader is not automatically a spec an agent can act on
Tools, Prompts, or Templates
Product Spec Template
A product spec without explicit scope produces an agent that interprets user need as an invitation to build the most ambitious version of the feature. The template below forces you to state what is in scope, what is out, and what a successful outcome looks like for users, before any design or implementation begins.
Worked Example: Slot Rescheduling
PRODUCT SPEC
============
Feature Name: Slot Rescheduling
Status: Ready for scoping
User Need:
Clients who have confirmed bookings sometimes need to change the time
without cancelling and rebooking. Currently they must cancel, return
to the booking page, and start over. This creates unnecessary drop-off
and generates cancellation notifications that feel disruptive to hosts.
Features and Scope:
In scope:
- Client requests a reschedule via a link in their confirmation email
- Client selects a new available slot from the host's current availability
- A rescheduled confirmation email is sent to both host and client
- The original slot is released and the new slot is confirmed atomically
Out of scope (v1):
- Host-initiated rescheduling
- Rescheduling with a payment change
- Rescheduling within 2 hours of the original slot start time
- Waitlist promotion when a slot is released by a reschedule
High-Level Acceptance Criteria:
- A client can reschedule using the link in their original confirmation email
- The new slot must be within the host's current published availability
- Both host and client receive a rescheduled confirmation email
- The original slot becomes available to other clients immediately
- A client cannot reschedule the same booking more than once within 24 hours
Generic Template
PRODUCT SPEC
============
Feature Name: [Human-readable feature name]
Status: [Discovery | Ready for scoping | Ready for design]
User Need:
[2–4 sentences: what problem does this feature solve, for whom, and
what does the current experience look like without it? Ground it in
a real user situation.]
Features and Scope:
In scope:
- [Specific capability included in this version]
- [Specific capability included in this version]
Out of scope:
- [Capability explicitly deferred to a future version]
- [Related feature handled elsewhere or not yet decided]
High-Level Acceptance Criteria:
- [Observable, user-facing outcome that confirms the feature works]
- [Observable, user-facing outcome that confirms the feature works]
- [Boundary condition: what the feature must not do]
Key fields:
User Need: answers the question "why are we building this?" before any agent touches the designOut of scope: the most leverage you have over agent scope creep; state it explicitly before any design beginsHigh-Level Acceptance Criteria: user-facing, not system-facing; these become the top-level review criteria for the whole feature
Functional Spec Template
A functional spec without explicit business rules produces code that handles the happy path and ignores everything that breaks it. The template below forces you to state user flows, system behaviour at each step, and the rules the system must enforce, before implementation begins. This is the layer where most feature failures originate: not in the code, but in the gap between what was built and what the system was supposed to do.
Worked Example: Client Booking Flow
FUNCTIONAL SPEC
===============
Feature Name: Client Booking Flow
Route: /book/{host-slug}
Status: Ready for implementation
User Flow:
1. Client navigates to /book/{host-slug}
2. Page loads host profile (name, avatar, short bio) and a date picker
3. Client selects a date; available 30-minute slots load for that date
4. Client selects a slot; a 60-second hold is placed on that slot to
prevent another client from booking it during checkout
5. Client enters their name and email address and clicks "Confirm Booking"
6. System validates the slot is still held and not booked by another client
7. System creates a booking record and releases the hold
8. System dispatches confirmation emails to host and client
9. Client is redirected to /confirmation/{booking-id}
System Behaviour:
- Slot availability is calculated in real time from the host's availability
windows, excluding confirmed bookings and synced calendar events
- A slot that is on hold (60-second checkout window) appears as unavailable
to other clients during that period
- If the hold expires before the client confirms, the slot is released
automatically and the client sees an inline error prompting them to
re-select
- If the slot is booked between hold placement and confirmation (race
condition), the client receives an inline error and the slot list refreshes
automatically
Business Rules:
- A client cannot book a slot that starts in the past
- A slot that already has a confirmed booking cannot be booked again
- Confirmation email must dispatch within 30 seconds of booking creation
- If no slots are available for the selected date, display:
"No available slots on this date. Try another day."
- Page must be accessible without authentication; hosts are identified
by their public slug
Edge Cases:
- Host slug not found: render a 404 page with a return link
- All slots on the selected date are held or booked: show the
"no available slots" message, not an empty list
- Email dispatch fails: log the failure, mark notification_sent = false,
retry via background job; do not block the booking confirmation flow
Generic Template
FUNCTIONAL SPEC
===============
Feature Name: [Human-readable name]
Route: [URL pattern, if applicable]
Status: [Draft | Ready for implementation | Under review]
User Flow:
1. [Actor + action at step 1]
2. [Actor + action at step 2]
3. [Continue until the flow reaches its successful end state]
System Behaviour:
- [What the system does at a specific step or in response to a condition]
- [What the system does when a constraint is triggered]
- [What the system does when the happy path is interrupted]
Business Rules:
- [Rule the system must enforce, stated as a constraint on system action]
- [Rule the system must enforce, stated as a constraint on system action]
- [User-facing message that must appear in a specific condition]
Edge Cases:
- [Error condition]: [exact system behaviour or user-facing message]
- [Error condition]: [exact system behaviour or user-facing message]
Key fields:
User Flow: numbered steps prevent the agent from inventing a different interaction modelBusiness Rules: stated as constraints on the system, not descriptions of the UI; the agent enforces these in codeEdge Cases: the most commonly skipped section; an agent will not generate robust error handling without them
Technical Spec Template
A technical spec without explicit architecture decisions produces code that works in isolation but conflicts with the rest of the system. The template below forces you to state the data model, the API surface, the concurrency approach, and the technology constraints before a coding agent writes a single line. Without it, the agent makes architecture decisions on its own.
Worked Example: Booking Reservation Endpoint
TECHNICAL SPEC
==============
Component Name: Booking Reservation Endpoint
Status: Ready for implementation
Architecture:
The booking reservation handler lives in the service layer, not the
route handler. The route handler validates the request and delegates
to BookingService.reserve(slotId, clientData). The service layer
owns all business logic, database operations, and job enqueueing.
Data Model:
bookings
id UUID primary key
host_id UUID foreign key → hosts.id
client_name VARCHAR(200) not null
client_email VARCHAR(200) not null
start_time TIMESTAMPTZ not null
end_time TIMESTAMPTZ not null
status ENUM ('pending', 'confirmed', 'cancelled')
notification_sent BOOLEAN default false
created_at TIMESTAMPTZ default now()
slot_holds
id UUID primary key
slot_start TIMESTAMPTZ not null
host_id UUID foreign key → hosts.id
held_until TIMESTAMPTZ not null (now + 60 seconds)
session_token UUID identifies the checkout session holding this slot
API Endpoints:
POST /api/bookings
Request: { host_slug, slot_start, client_name, client_email }
Response: 201 { booking_id, confirmation_url }
409 { error: "slot_unavailable", message: "..." }
422 { error: "validation_error", fields: [...] }
POST /api/slot-holds
Request: { host_slug, slot_start, session_token }
Response: 201 { held_until }
409 { error: "slot_unavailable" }
Concurrency Approach:
Slot reservation uses a row-level lock on the slot_holds record:
BEGIN;
SELECT id FROM slot_holds
WHERE host_id = $1 AND slot_start = $2
FOR UPDATE NOWAIT;
-- if row exists and held_until > now(): return 409
-- if row does not exist: insert hold, proceed with booking
COMMIT;
This prevents two concurrent requests from reserving the same slot.
NOWAIT is used so the second request fails immediately rather than
waiting for the lock.
System Integrations:
- Notification service: enqueue booking_id to the confirmation email
queue on successful booking; do not call the email service inline
- Calendar sync: enqueue booking_id to the calendar sync queue after
booking is committed; do not await the sync result
Tech Stack Constraints:
- Backend: Node.js 20, Express 4, node-postgres (pg)
- Database: PostgreSQL 15; parameterised queries only (no string
interpolation in any SQL)
- ORM: none; raw SQL via node-postgres query builder
- Timezone: all times stored as TIMESTAMPTZ (UTC); display conversion
happens on the frontend
Generic Template
TECHNICAL SPEC
==============
Component Name: [Human-readable name]
Status: [Draft | Ready for implementation | Under review]
Architecture:
[2–4 sentences: how does this component fit into the existing system?
Which layer does it live in? What does it delegate to and what does
it receive from?]
Data Model:
[table_name]
[column_name] [type] [constraints and notes]
[column_name] [type] [constraints and notes]
API Endpoints:
[METHOD] [/path]
Request: [payload shape]
Response: [status code + payload for each outcome]
Concurrency Approach:
[If this component handles concurrent requests or writes: describe the
locking or isolation strategy. State it explicitly; do not leave it
for the agent to decide.]
System Integrations:
- [Service or system this component calls, and how (sync/async/queue)]
- [External API or queue, and what gets enqueued or called]
Tech Stack Constraints:
- [Language, framework, and version]
- [Database technology and query pattern]
- [Performance or security constraint]
- [Convention that must be followed: naming, file location, pattern]
Key fields:
Data Model: column names and types must match the actual schema; mismatches here produce code that fails at the database boundaryConcurrency Approach: the most commonly skipped section; agents will not implement locking without explicit instructionSystem Integrations: state sync vs. async explicitly; agents default to synchronous calls when async is required
Test Spec Template
A test spec derived from the functional spec produces verification that confirms the system behaves as designed. A test spec written from the implementation produces tests that pass when the code is wrong in the same way as the tests. The template below forces you to derive test cases from functional requirements, cover UI, API, and integration paths, and state expected outcomes before any test code is written.
Worked Example: Booking Confirmation Tests
TEST SPEC
=========
Feature: Client Booking Flow (see functional spec)
Status: Ready for test generation
Test Cases:
UI Scenarios:
TC-01: Single client books available slot
Setup: Host has one available slot on Wednesday at 14:00
Action: Client navigates to booking page, selects Wednesday 14:00,
enters name and email, clicks "Confirm Booking"
Expected: Booking record created with status "confirmed"; client
redirected to /confirmation/{booking-id}; confirmation
email dispatched to both parties
TC-02: Two clients race for the last slot
Setup: Host has one available slot at 14:00; two clients load
the booking page simultaneously
Action: Both clients select 14:00 and submit within the same second
Expected: One client receives confirmation and is redirected;
second client sees inline error "This slot was just booked.
Please select another time." and the slot list refreshes
with 14:00 no longer available
TC-03: Client selects a date with no available slots
Setup: All of the host's Wednesday slots are confirmed bookings
Action: Client selects Wednesday on the date picker
Expected: "No available slots on this date. Try another day."
message is displayed; no empty slot grid is rendered
API Scenarios:
TC-04: POST /api/bookings returns 201 on valid request
Input: { host_slug: "dr-chen", slot_start: "2026-06-10T14:00:00Z",
client_name: "Alice Lee", client_email: "[email protected]" }
Expected: 201 { booking_id: "<uuid>", confirmation_url: "/confirmation/<uuid>" }
booking record in database with status "confirmed"
TC-05: POST /api/bookings returns 409 on conflict
Setup: Slot at 14:00 has a confirmed booking
Input: Same slot_start as confirmed booking
Expected: 409 { error: "slot_unavailable",
message: "This slot is no longer available." }
No new booking record created
TC-06: POST /api/bookings returns 422 on invalid email
Input: { client_email: "not-an-email" }
Expected: 422 { error: "validation_error",
fields: [{ field: "client_email",
message: "Invalid email address" }] }
Integration Scenarios:
TC-07: Confirmation email delivered within 10 seconds
Action: Complete a booking via POST /api/bookings
Expected: Email to client and host address appears in test inbox
within 10 seconds of the 201 response
TC-08: Email failure does not block booking confirmation
Setup: Email service returns 503 on first attempt
Action: Complete a booking
Expected: Booking record has status "confirmed"; notification_sent = false;
retry job enqueued; 201 response returned to client without delay
Validation Rules:
- booking.status must be "confirmed" immediately after POST /api/bookings 201
- notification_sent must not be set to true until email dispatch is confirmed
by the email service (not just enqueued)
- slot_holds record for the booked slot must be deleted after booking creation
- No booking record should exist in the database after a 409 response
Generic Template
TEST SPEC
=========
Feature: [Feature name and reference to functional spec]
Status: [Draft | Ready for test generation | Active]
Test Cases:
UI Scenarios:
[TC-ID]: [Scenario name]
Setup: [Initial state of the system before the action]
Action: [What the user does, step by step]
Expected: [Exact observable outcome: what the user sees, what
changes in the system]
API Scenarios:
[TC-ID]: [Scenario name]
Input: [Request payload or parameters]
Expected: [HTTP status code + response body + database state]
Integration Scenarios:
[TC-ID]: [Scenario name]
Action: [What triggers the integration]
Expected: [Observable outcome across system boundaries: email,
calendar event, queue entry, etc.]
Validation Rules:
- [Database invariant: a field or record state that must hold after
every successful or failed operation]
- [System invariant: a condition that must never be violated under
any test scenario]
Key fields:
Setup: specifies the exact system state before each test; agents without setup context generate tests with undeclared dependenciesExpected: one observable outcome per test case, stated precisely enough that pass/fail is unambiguousValidation Rules: database and system invariants that apply across all test cases, not just the happy path
Actionable Takeaways
- Pick one task you are currently working on and identify which spec type it requires before writing a single line of code or a prompt.
- Write the
Out of Scopesection of your product spec first. It forces you to make exclusion decisions early, when they are cheap. - When writing a functional spec, list business rules and edge cases before the user flow. Rules that emerge from the flow are easier to catch before you write the steps than after.
- When writing a technical spec, state the concurrency approach explicitly. If you are uncertain what it should be, write "TBD: locking strategy" -- that gap will surface the ambiguity rather than hiding it.
- Derive test cases from the functional spec, not the code. Each test case should reference a business rule or user flow step; tests that reference implementation details drift when the code changes.
Practical Examples
Example 1: Product Manager Scoping a Feature (Product Spec)
A product manager is asked to scope the team scheduling feature: the ability for an organisation to share a unified booking page across multiple hosts. She writes a product spec before any design begins.
The user need section explains the problem (clients booking with a team do not know which host is available; they currently contact the team by email to arrange a time). The scope section defines what is in v1 (a team booking page where the system selects the next available host) and what is not (client preference for a specific host, round-robin balancing rules, custom team branding). The acceptance criteria state three observable outcomes: a client can book without knowing individual host names; the system assigns the booking to the first available host; both the assigned host and the client receive a confirmation.
The agent working from this spec scopes the feature correctly. The team booking page it produces handles the assignment flow without building host preference selection, which was explicitly out of scope.
Example 2: Engineering Lead Speccing System Behaviour (Functional Spec)
An engineering lead hands off the cancellation flow to a developer using a coding agent. Before any code is written, she writes a functional spec.
The user flow covers the full interaction: client navigates to their booking, selects cancel, confirms, and is redirected to a cancellation confirmation page. The system behaviour section specifies the sequence: slot is released immediately on cancellation, not on email delivery. The business rules include the late-cancellation rule: a cancellation within 24 hours of the booking start time adds a flag to the client's account, and the host can waive the flag manually. Edge cases cover what happens if the cancellation link has expired, if the booking is already cancelled, and if the email service is unavailable.
The agent generates the cancellation handler with the correct sequencing -- slot release before notification -- and implements the late-cancellation flag as an explicit check rather than an assumption.
Example 3: Senior Engineer Defining the Architecture (Technical Spec)
A senior engineer is building the Google Calendar sync adapter. Before directing the coding agent, she writes a technical spec.
The architecture section defines where the adapter lives (an isolated CalendarSyncAdapter class, called via a job queue event after booking confirmation, not inline in the booking handler). The data model section defines the CalendarSync table: internal booking ID mapped to external Google Calendar event ID for idempotency. The API section specifies the Google Calendar Events API v3 endpoint, the OAuth 2.0 flow, and how token refresh is handled. The concurrency approach covers what happens on retry: because the booking ID is mapped to a calendar event ID, a second call for the same booking checks the mapping table before creating a new event. The tech constraints include that no new dependencies can be introduced beyond the existing Google API client library.
The agent generates the adapter with correct idempotency handling because the technical spec stated the mapping approach explicitly. Without the spec, the agent would have generated a naive implementation that creates duplicate calendar events on retry.
Example 4: QA Lead Writing Test Scenarios (Test Spec)
A QA lead writes a test spec for the group booking feature after the functional spec is approved, before any implementation begins.
The UI scenarios section includes the concurrent booking case: two clients attempt to book the last spot simultaneously, one receives a confirmation, the other sees a real-time "session full" message. The API scenarios section specifies the concurrent load test: POST /group-sessions/{id}/bookings with 10 simultaneous requests should return exactly one 200 and nine 409 responses. The integration scenario verifies the host's calendar event appears within 5 seconds of the first confirmed booking. The validation rules section adds two database invariants: attendee count must never exceed max_attendees, verified after every API test run; and no booking record should exist for a 409 response.
The test-generation agent produces test code that maps to functional requirements rather than implementation internals. When the implementation changes, the tests are retargeted by updating the functional spec, not the test code.
Implementation Workflow
Review your notes from Lesson 3: The Four Spec Types. Identify one task you have not yet applied in practice.
Choose the spec type that matches the stage of work: use a Product Spec if you are defining what to build, a Functional Spec if you are describing system behaviour, a Technical Spec if you are specifying the implementation, or a Test Spec if you are defining verification scenarios.
Copy the Generic Template for that spec type from this lesson into a new file in your project.
Fill in every section. Do not skip
Out of Scope(Product Spec),Edge Cases(Functional Spec),Concurrency Approach(Technical Spec), orValidation Rules(Test Spec). These are the sections most commonly omitted and most commonly the cause of agent output that misses the real requirement.Before prompting the agent, read back what you have written. For each section, ask: if the agent reads only this section and ignores the rest, does it have enough to do the right thing? If not, add the missing information.
Hand the spec to the agent as its first message, prefixed with: "Act on the following spec. If any field is ambiguous, ask one clarifying question before proceeding." If the agent asks about something you already specified, your spec has a gap: fill it before running the next session.
Evaluate the output against the spec. For a product spec, check whether the scope boundary was respected. For a functional spec, check whether the business rules are enforced. For a technical spec, check whether the architecture matches. For a test spec, check whether each test case maps back to a stated requirement. Keep this output: you will compare it against a second iteration in Lesson 5: The Spec → Generate → Review Loop.