Write Your First Spec
Build a real spec for a Todo API. By the end you will have a file that any OpenSpec-compatible engine can parse, validate, and execute.
The smallest valid spec
An OpenSpec file is JSON, YAML, or TOON. In v1.1 the document is a single specification — there is no project wrapper and no specifications array. The root carries schemaVersion: "1.1" plus a handful of required fields: id, projectId, title, status, goals, requirements, architecture, scope, techStack, folderStructures, acceptanceCriteria, nonFunctionalRequirements, guardrails, epics, and blueprints. Here is the smallest valid Todo API spec:
{
"schemaVersion": "1.1",
"id": "spec-todo-api",
"projectId": "proj-todo",
"title": "Todo API",
"status": "planning",
"goals": [
{
"id": "goal-capture",
"title": "Capture todos quickly",
"description": "Let users create and list todos with minimal friction.",
"type": "user",
"successCriteria": ["A todo can be created and listed in under one second"]
},
{
"id": "goal-durable",
"title": "Keep todos durable",
"description": "Persist todos so they survive restarts.",
"type": "technical",
"successCriteria": ["No todo is lost across a service restart"]
},
{
"id": "goal-contract",
"title": "Expose a stable contract",
"description": "Offer a predictable REST surface clients can rely on.",
"type": "business",
"successCriteria": ["The public endpoints follow a documented contract"]
}
],
"requirements": [
{
"id": "req-create",
"title": "Create a todo",
"description": "Clients can create a todo with a title.",
"type": "functional",
"acceptanceCriteria": [
{
"id": "ac-create",
"given": "a valid todo payload",
"when": "the client POSTs to /todos",
"then": "the todo is stored and returned with a generated id",
"order": 1
}
]
},
{
"id": "req-list",
"title": "List todos",
"description": "Clients can retrieve all todos.",
"type": "functional",
"acceptanceCriteria": [
{
"id": "ac-list",
"given": "stored todos exist",
"when": "the client GETs /todos",
"then": "all todos are returned in creation order",
"order": 1
}
]
},
{
"id": "req-validate",
"title": "Reject invalid input",
"description": "Malformed payloads are rejected clearly.",
"type": "business-rule",
"acceptanceCriteria": [
{
"id": "ac-validate",
"given": "a payload without a title",
"when": "the client POSTs to /todos",
"then": "the request is rejected with a 400",
"order": 1
}
]
}
],
"architecture": "A stateless HTTP service backed by a relational database, exposing a small REST API for todos.",
"scope": {
"inScope": ["Creating todos", "Listing todos", "Input validation"],
"outOfScope": ["Authentication and multi-user accounts"]
},
"techStack": [],
"folderStructures": [
{
"id": "fs-service",
"scope": "service",
"content": "src/\n routes/todos.ts\n db/index.ts\n server.ts"
}
],
"acceptanceCriteria": [],
"nonFunctionalRequirements": [],
"guardrails": [],
"epics": [],
"blueprints": []
}Save this as todo-api.oschema.json. That's a valid OpenSpec spec. Everything else builds on this skeleton.
Goals and requirements are structured
In v1.1, goals and requirements are no longer plain strings — each is a typed object. A goal declares a type and successCriteria; a requirement carries acceptanceCriteria written as Given / When / Then so an agent has a concrete checklist to verify against.
{
"goals": [
{
"id": "goal-capture",
"title": "Capture todos quickly",
"description": "Let users create and list todos with minimal friction.",
"type": "user",
"successCriteria": ["A todo can be created and listed in under one second"]
}
],
"requirements": [
{
"id": "req-create",
"title": "Create a todo",
"description": "Clients can create a todo with a title.",
"type": "functional",
"acceptanceCriteria": [
{
"id": "ac-create",
"given": "a valid todo payload",
"when": "the client POSTs to /todos",
"then": "the todo is stored and returned with a generated id",
"order": 1
}
]
}
]
}Shared patterns
Shared patterns capture conventions that apply across tickets. They prevent style drift by giving agents a single source of truth for code standards, common imports, and return types.
{
"sharedPatterns": [
{
"id": "sp-rest",
"name": "REST conventions",
"description": "All endpoints return JSON and use plural resource URLs.",
"codeStandards": {
"naming": "camelCase for fields, plural nouns for routes",
"errorHandling": "Return a typed Result at module boundaries"
},
"commonImports": ["import { Result, ok, err } from '../shared/result'"],
"returnTypes": { "handler": "Promise<Result<Response, AppError>>" }
}
]
}Blueprints
Blueprints are design artifacts — diagrams, schemas, ADRs — referenced by tickets. Each has a category, a format, and a content body. They keep design decisions discoverable instead of buried in chat logs or document folders.
{
"blueprints": [
{
"id": "bp-db-schema",
"title": "Database schema",
"category": "erd",
"format": "mermaid",
"content": "erDiagram\n TODO {\n uuid id PK\n string title\n bool completed\n }"
},
{
"id": "bp-api-contract",
"title": "API contract",
"category": "api",
"format": "markdown",
"content": "GET /todos -> 200 [Todo]\nPOST /todos -> 201 Todo"
}
]
}Tickets — The atomic unit of agent work
Tickets live inside an epic and are the smallest piece of work in a spec. Each declares a ticketType, a complexity, an estimatedMinutes budget, Given / When / Then acceptanceCriteria, ordered implementationSteps, and the files it will touch — so the implementing agent knows exactly what to build and how it will be checked.
{
"id": "ticket-create-todo",
"epicId": "epic-core",
"title": "Implement POST /todos",
"description": "Create a todo. Validate the title is non-empty. Return 201 with the created resource.",
"ticketType": "implementation",
"complexity": "small",
"estimatedMinutes": 90,
"acceptanceCriteria": [
{
"id": "ac-create-201",
"given": "a payload with a title",
"when": "POST /todos is called",
"then": "a 201 is returned with the created todo",
"order": 1
},
{
"id": "ac-create-400",
"given": "a payload without a title",
"when": "POST /todos is called",
"then": "a 400 is returned with an error envelope",
"order": 2
}
],
"implementationSteps": [
{ "id": "step-1", "text": "Add the POST /todos route handler", "order": 1 },
{ "id": "step-2", "text": "Validate the payload and persist the todo", "order": 2 }
],
"filesToBeCreated": ["src/routes/todos.ts"],
"blueprintReferences": [
{ "blueprintId": "bp-api-contract", "context": "Create path" }
],
"dependencies": []
}Dependencies — The execution graph
Tickets can declare dependencies on other tickets using a target ticketId and a type. OpenSpec supports two dependency types:
requires— this ticket needs the other to be complete before it can start.blocks— this ticket prevents the other from starting until it is complete.
[
{
"id": "ticket-create-todo",
"title": "POST /todos",
"dependencies": []
},
{
"id": "ticket-list-todos",
"title": "GET /todos",
"dependencies": [
{ "ticketId": "ticket-create-todo", "type": "requires" }
]
},
{
"id": "ticket-delete-todo",
"title": "DELETE /todos/:id",
"dependencies": [
{ "ticketId": "ticket-create-todo", "type": "requires" }
]
}
]How an engine sees this spec
The dependency graph for our Todo API looks like this. An engine walks the graph to determine what can be worked on next.
ticket-create-todo has no dependencies, so it is actionable immediately. The other two require it, so they wait.