v1.1
Getting Started/Write Your First Spec

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
        }
      ]
    }
  ]
}
Why this matters: Agents hallucinate requirements when the spec is vague. Structured acceptance criteria give them an explicit pass/fail checklist instead of a paragraph to interpret. See: Hallucinated Requirements →

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>>" }
    }
  ]
}
Why this matters: Without shared patterns, each agent (or developer) invents their own conventions. The spec becomes inconsistent over time. See: Style Drift →

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"
    }
  ]
}
Why this matters: Design artifacts that live outside the spec get lost or go stale. Blueprints keep them version-controlled and linked to the tickets that implement them. See: Orphan Design Artifacts →

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": []
}
Why this matters: Agents hallucinate requirements when the spec is vague. Acceptance criteria give them a concrete checklist to verify against. See: Hallucinated Requirements →

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" }
    ]
  }
]
Why this matters: Without explicit dependency data, an engine might start a ticket whose prerequisites are not done yet. The validator catches circular dependencies and dangling references at lint time. See: Work That Shouldn't Exist Yet →

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
ticket-list-todos
ticket-delete-todo

ticket-create-todo has no dependencies, so it is actionable immediately. The other two require it, so they wait.