Skip to main content

Writing Machine-Readable Specs for AI Tools

A machine-readable spec is a formal description of software behavior, structure, and constraints that can be parsed and validated by tools—and crucially, interpreted by AI systems. Unlike prose requirements, machine-readable specs use structured formats (JSON Schema, OpenAPI, YAML, Protocol Buffers) to define data shapes, API contracts, state machines, and invariants with no ambiguity. When you write specs this way, AI agents can read them, generate code from them, and verify implementations against them, all automatically.

The Anatomy of a Machine-Readable Spec

A complete spec has five parts:

  1. Structure: What data types exist? What fields must every object have?
  2. Constraints: What rules govern valid values? (ranges, formats, enums, dependencies)
  3. Behavior: What functions or endpoints exist? What do they do?
  4. Examples: Concrete input/output samples to eliminate interpretation guessing.
  5. Validation rules: How do you know an implementation satisfies the spec?

Each part must be precise, machine-parseable, and tested.

Data Structure Specifications (JSON Schema)

JSON Schema is the most widely-adopted machine-readable format for defining data. An AI reading a JSON Schema immediately understands what payloads are valid.

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "User",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^user_[a-z0-9]{16}$",
"description": "Unique user identifier, prefixed with 'user_'"
},
"email": {
"type": "string",
"format": "email",
"minLength": 5,
"maxLength": 254,
"description": "User email address, must be valid RFC 5322"
},
"accountStatus": {
"type": "string",
"enum": ["active", "suspended", "deleted"],
"description": "Current account status"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "Account creation time in ISO 8601 format"
},
"tags": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 50
},
"maxItems": 10,
"description": "Optional user tags, max 10, each 1-50 chars"
}
},
"required": ["id", "email", "accountStatus", "createdAt"],
"additionalProperties": false
}

An AI reading this schema understands:

  • id must match the regex (not arbitrary strings)
  • email is validated as RFC 5322 compliant
  • accountStatus is restricted to three values
  • tags is optional, but if present, must be an array of 1-50 char strings, max 10 items
  • No extra fields are allowed

The schema acts as both documentation and a machine-checkable contract.

API Specifications (OpenAPI)

For REST or HTTP-based APIs, OpenAPI (formerly Swagger) is the standard. An OpenAPI spec defines every endpoint, method, request/response schema, and error case:

openapi: 3.0.0
info:
title: User Management API
version: 1.0.0
description: API for user account management and authentication
servers:
- url: https://api.example.com/v1
paths:
/users:
post:
summary: Create a new user
operationId: createUser
tags: [Users]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
description: Password must be at least 8 characters
firstName:
type: string
minLength: 1
maxLength: 100
required: [email, password, firstName]
responses:
'201':
description: User successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Invalid input (email format, weak password)
'409':
description: User already exists with this email
'500':
description: Server error
/users/{userId}:
get:
summary: Retrieve a user by ID
operationId: getUser
parameters:
- name: userId
in: path
required: true
schema:
type: string
pattern: '^user_[a-z0-9]{16}$'
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
components:
schemas:
User:
$ref: '#/components/schemas/User'

An AI given this spec can generate:

  • Request validation code (reject emails that don't match RFC 5322, passwords under 8 chars)
  • Endpoint handlers that accept the specified inputs and return the specified schemas
  • Client code that calls the endpoints correctly
  • Integration tests that cover all response codes

Behavior and State Specifications

For complex workflows, you may need to specify state transitions or behavior rules. A state machine spec, often in YAML or JSON, clarifies what actions are valid in each state:

StateMachine:
name: OrderProcessing
initialState: pending
states:
pending:
description: Order created, awaiting payment
transitions:
- event: pay
target: paid
preconditions:
- payment amount must be >= order total
- event: cancel
target: cancelled
paid:
description: Payment received, preparing shipment
transitions:
- event: ship
target: shipped
- event: refund
target: refunded
preconditions:
- can only refund within 30 days of payment
shipped:
description: Order dispatched
transitions:
- event: deliver
target: delivered
- event: return
target: returned
delivered:
description: Order received by customer
transitions: []
cancelled:
description: Order cancelled
transitions: []
refunded:
description: Payment refunded to customer
transitions: []

This spec tells an AI (and humans) what transitions are legal. Code generated from this spec will reject invalid transitions like "ship an order in pending state".

Constraint Languages

For critical systems (finance, security), you might specify invariants that must always hold. A simple constraint notation:

Invariants for BankAccount:
- balance >= 0 always
- balance <= creditLimit always
- for every Withdrawal(amount):
- amount > 0
- new_balance = balance - amount
- assert new_balance >= 0 else reject
- for every Deposit(amount):
- amount > 0
- new_balance = balance + amount

An AI reading this understands not just what operations exist, but what checks must pass before accepting them.

Examples and Test Vectors in Specs

Specs are only as good as their examples. Always include:

  1. Minimal valid example: The smallest instance that satisfies the spec.
  2. Rich example: Shows all optional fields and edge cases.
  3. Invalid examples (commented): What should be rejected and why.
# Example: User creation request
examples:
minimal_valid:
email: "[email protected]"
password: "secure1234"
firstName: "Alice"

full:
email: "[email protected]"
password: "SuperSecure9876"
firstName: "Robert"
lastName: "Smith"
tags: ["vip", "early-adopter"]

invalid_password_too_short:
# REJECT: password is only 6 chars, spec requires 8
email: "[email protected]"
password: "short1"
firstName: "Charlie"

invalid_email_format:
# REJECT: not a valid email
email: "not-an-email"
password: "ValidPass123"
firstName: "Dan"

These examples are machine-testable. An AI generating code sees "oh, I must validate email format and password length."

Versioning and Compatibility Specs

When APIs evolve, you must document what breaks and what's backward-compatible:

version: "2.0.0"
deprecations:
- field: "legacyId"
removedInVersion: "3.0.0"
replacement: "id"
- endpoint: "POST /register"
removedInVersion: "3.0.0"
replacement: "POST /users"
breakingChanges:
- "User.email field format now requires RFC 5322 compliance (was any string)"
- "GET /users now returns paginated results; page and limit params required"

An AI aware of version compatibility can generate migration helpers and deprecation warnings.

Best Practices for Machine-Readable Specs

Be precise, not clever. "approximately 5 chars" is ambiguous. "minLength: 3, maxLength: 7" is clear.

Include units. "Timeout: 5000" is vague. "Timeout: 5000ms" or "Timeout: 5 seconds" is actionable.

Define every enum. Never assume an AI knows what "status: active" implies. List all valid statuses.

Specify error cases explicitly. Mention all HTTP codes or exception types. Don't assume "400 for bad input" is sufficient; specify what makes input bad.

Use references (anchors) to avoid duplication. If many endpoints return a User object, define it once and reuse it with $ref.

Validate your specs. Use tools like ajv-cli (JSON Schema), swagger-cli (OpenAPI) to catch syntax errors before sharing with AI.

Comparison Table: Spec Formats

FormatBest ForParseableAI-FriendlyMaturity
JSON SchemaData validationYesYesMature
OpenAPIREST APIsYesYesVery Mature
GraphQL SchemaQuery APIsYesYesMature
Protocol BuffersData serializationYesYesMature
YAML (custom)Domain-specificPartialWith trainingVaries
Prose + examplesOnboardingNoNoAlways

Key Takeaways

  • Machine-readable specs use structured formats (JSON Schema, OpenAPI, YAML) that both humans and AI can parse without guessing.
  • Complete specs include structure, constraints, behavior, examples, and validation rules.
  • Examples and test vectors in specs are machine-executable; they guide AI code generation and catch errors early.
  • Versioning and deprecation info in specs prevents AI from generating code against outdated contracts.
  • Precise specs (ranges, formats, enums) reduce the number of rounds of prompting and regeneration.

Frequently Asked Questions

Can I use Markdown as a spec format?

Markdown alone is not machine-readable. However, well-structured Markdown with frontmatter and fenced code examples can serve as a spec template. Pair it with JSON Schema or OpenAPI blocks to make it machine-parseable.

What's the minimum viable spec?

At minimum: a schema (what data exists), examples (what valid data looks like), and constraints (what makes data invalid). You can start with JSON Schema examples and OpenAPI for APIs; expand from there.

How do I keep specs in sync with code?

Treat specs as the source of truth. Use spec-aware code generators or linters that reject code deviating from the spec. Version control specs alongside code. Use CI checks that validate code against specs.

Can AI generate specs from code?

Yes, but it's error-prone. AI can infer structure (fields, types) but often misses constraints, edge cases, and intent. Manual review is essential. Better: write specs first, generate code, then iterate.

Further Reading