---
title: "OpenAPI for Agents"
description: "Writing OpenAPI specifications that AI agents can use as executable contracts"
url: "https://agentsurface.dev/docs/api-surface/openapi-for-agents"
lastVerified: 2026-09-25
lastModified: 2026-09-25T05:46:01.000Z
---



## Summary [#summary]

OpenAPI specs are executable contracts when agents consume them. This document explains how spec semantics - operationId, descriptions, schema constraints - directly shape agent behavior at runtime. Vague descriptions become routing failures. Missing operationId values break tool generation.

* operationId must be globally unique, verb-first, and in one consistent case
* Descriptions are prompts, not documentation - be precise and specific
* Schema constraints are enforced strictly by agents
* Ambiguous descriptions lead to silent failures and misrouting
* Token-heavy prose degrades LLM routing accuracy

When an agent framework ingests your spec to generate tool definitions, every field becomes executable instructions. A vague description is a bug. A missing `operationId` is a build failure. This page covers what makes an OpenAPI spec agent-ready.

## Specs as Agent Contracts [#specs-as-agent-contracts]

An OpenAPI spec written for human developers communicates intent through prose. A developer reads the description, infers the behavior, and writes code. That pipeline breaks entirely when an agent is the consumer.

Agents pattern-match: every decision about when to call an operation, what parameters to pass, and how to interpret the response is based on the literal text you wrote, so the spec is a contract the agent executes verbatim.

The practical consequences:

* **Ambiguity compounds.** If a description could be interpreted two ways, the agent will pick one, and you will not know which until a user reports a bug.
* **Descriptions are prompts.** The LLM generating tool calls reads your `description` as part of its context window. Token-heavy, prose-heavy descriptions degrade routing accuracy.
* **Schema constraints are enforced, not suggested.** Unlike a human who reads "string, max 100 chars" and uses judgment, an agent runtime validates strictly and rejects calls that violate the schema.

## Key Fields [#key-fields]

### `operationId` [#operationid]

`operationId` is the function name in every generated client, SDK, and tool definition. It must be:

* **Globally unique** across your entire spec
* **Verb-noun format** that unambiguously describes the action and resource
* **Snake\_case or camelCase** - pick one and be consistent

```yaml
# Bad - no operationId, frameworks generate names from path + method
paths:
  /users/{id}:
    get:
      summary: Get user

# Bad - noun only, ambiguous between list and get
paths:
  /users/{id}:
    get:
      operationId: user

# Good - verb_noun, unambiguous
paths:
  /users/{id}:
    get:
      operationId: get_user
  /users:
    get:
      operationId: list_users
    post:
      operationId: create_user
  /users/{id}/activate:
    post:
      operationId: activate_user
```

Common verb prefixes: `get_`, `list_`, `create_`, `update_`, `delete_`, `search_`, `send_`, `trigger_`, `validate_`, `generate_`.

### `description` [#description]

The `description` field on an operation is the primary signal an agent uses to decide whether to call it - it becomes the generated tool's description verbatim. The rules for writing that description as a routing prompt (what it does, when to use it, when not to, prerequisites and edge cases) are OpenAPI-agnostic and live in [Naming and Descriptions: Descriptions as Prompts](/docs/tool-design/naming-and-descriptions#descriptions-as-prompts); apply them here exactly as written for MCP tools.

```yaml
# Bad - describes the endpoint, not the agent's decision
description: Returns a user object from the database by user ID.

# Good - trigger condition, disambiguation, return summary
description: >
  Retrieves a single user's full profile including account settings,
  subscription tier, and usage statistics. Use this when you have a
  specific user ID and need complete profile data. For listing multiple
  users or searching by email or name, use list_users instead. Returns
  404 if the user does not exist or has been deleted.
```

### `summary` [#summary-1]

`summary` is a single-line label shown in tool pickers and sidebars. It should be a complete human-readable sentence under 100 characters. Do not repeat the `operationId`.

```yaml
# Bad - repeats the operationId
summary: get_user

# Bad - too long
summary: Retrieves a user object from the database given a valid user identifier

# Good
summary: Get a user's full profile by ID
```

## Parameter Design [#parameter-design]

### Count and Depth [#count-and-depth]

Fewer parameters make tool selection more accurate. Operations with more than 10 parameters suffer from agent confusion about which parameters are required, which interact, and what the defaults mean.

* **Ideal:** fewer than 4 parameters
* **Acceptable:** 4–10 parameters
* **Problematic:** more than 10 parameters

When an operation genuinely requires many inputs, group related parameters into a request body object rather than passing them as individual query parameters.

### Flat over Nested [#flat-over-nested]

Agents handle flat parameter lists more reliably than deeply nested objects. Nesting creates ambiguity about which level holds which constraint.

```yaml
# Bad - nested, agent must navigate structure
requestBody:
  content:
    application/json:
      schema:
        type: object
        properties:
          user:
            type: object
            properties:
              profile:
                type: object
                properties:
                  name:
                    type: string
                  email:
                    type: string

# Better - flat structure with clear field names
requestBody:
  content:
    application/json:
      schema:
        type: object
        required: [name, email]
        properties:
          name:
            type: string
            description: Full display name of the user
          email:
            type: string
            format: email
            description: Primary email address used for login and notifications
```

### Format Keywords [#format-keywords]

Use `format` to constrain string inputs. This allows validators to enforce constraints and gives agents semantic information without requiring description prose.

```yaml
properties:
  user_id:
    type: string
    format: uuid
    description: Unique identifier for the user
  created_after:
    type: string
    format: date-time
    description: ISO 8601 datetime; return only records created after this time
  report_type:
    type: string
    enum: [summary, detailed, raw]
    description: >
      Output verbosity. Use "summary" for dashboard display, "detailed"
      for export workflows, "raw" for data pipeline ingestion.
```

### Exhaustive Enums [#exhaustive-enums]

List every valid value in the `enum` array. Do not use open-ended strings when a fixed set of values is accepted. Each enum value should have a corresponding description in the parent field's `description`, especially when the semantic difference between values is not obvious from the name alone.

```yaml
properties:
  status:
    type: string
    enum: [pending, active, suspended, cancelled, deleted]
    description: >
      Account status filter. "pending" = awaiting email verification.
      "active" = fully operational. "suspended" = temporarily disabled
      by admin. "cancelled" = user-initiated cancellation, data retained.
      "deleted" = scheduled for permanent deletion within 30 days.
```

## Response Schema Design [#response-schema-design]

### Essential Fields Only [#essential-fields-only]

Include only fields agents need to make decisions or pass to subsequent operations. Avoid exposing large internal objects with dozens of fields - most will never be read and inflate token usage.

```yaml
# Bad - exposes every database column
responses:
  '200':
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/UserFull'  # 40+ fields

# Good - returns the fields agents actually use
responses:
  '200':
    content:
      application/json:
        schema:
          type: object
          required: [id, email, status, created_at]
          properties:
            id:
              type: string
              format: uuid
            email:
              type: string
              format: email
            status:
              type: string
              enum: [active, suspended, deleted]
            display_name:
              type: string
            created_at:
              type: string
              format: date-time
```

### Concrete Examples [#concrete-examples]

Examples are part of the contract. Include them at the operation level so agents see a real response shape alongside the abstract schema.

```yaml
responses:
  "200":
    content:
      application/json:
        schema:
          $ref: "#/components/schemas/User"
        example:
          id: "usr_01HV3K8MNPQR2STUVW"
          email: "alice@example.com"
          status: "active"
          display_name: "Alice Chen"
          created_at: "2024-03-15T09:23:11Z"
```

### Error Documentation [#error-documentation]

Document every error response your operation can return. An undocumented 422 is an unrecoverable failure for an agent that has no idea what it did wrong.

```yaml
responses:
  "200":
    description: User found and returned
  "400":
    description: Invalid user_id format (must be UUID)
  "404":
    description: User not found or has been permanently deleted
  "429":
    description: Rate limit exceeded; retry after the Retry-After header value
  "500":
    description: Internal server error; safe to retry with exponential backoff
```

## Explicit Disambiguation [#explicit-disambiguation]

When operations have similar names or overlapping functionality, add explicit comparison language to their descriptions so agents don't call the wrong one - the same disambiguation rule as [Naming and Descriptions: Disambiguation Patterns](/docs/tool-design/naming-and-descriptions#disambiguation-patterns), applied to `operationId`/`description` pairs instead of MCP tool names:

```yaml
paths:
  /users/{id}:
    get:
      operationId: get_user
      description: >
        Returns the complete profile for a single user by ID. Use this
        when you have a specific user ID. For searching or listing multiple
        users, use list_users instead.
```

## Token Efficiency [#token-efficiency]

Every description field enters the context window when an agent is choosing which tool to call, so excessive length degrades routing accuracy the same way it does for any tool description - see [Naming and Descriptions: Length Guidelines](/docs/tool-design/naming-and-descriptions#length-guidelines) for the target ranges. Write descriptions that stay precise; paragraphs of edge cases are a signal the operation should be split.

## OpenAPI 3.1 Features That Matter [#openapi-31-features-that-matter]

OpenAPI 3.1 aligns with JSON Schema 2020-12, which enables several patterns important for agent-ready specs.

### Type Arrays [#type-arrays]

Allow a field to hold multiple types without a discriminator:

```yaml
properties:
  value:
    type: [string, "null"]
    description: The field value, or null if not yet set
```

### `$ref` with Sibling Keywords [#ref-with-sibling-keywords]

In 3.1, `$ref` can appear alongside other keywords:

```yaml
properties:
  user:
    $ref: "#/components/schemas/User"
    description: The owning user; see get_user for full field details
```

### Webhooks Field [#webhooks-field]

Document incoming webhooks in the same spec as your outbound API:

```yaml
webhooks:
  order.completed:
    post:
      operationId: webhook_order_completed
      description: >
        Sent when an order transitions to "completed" status.
        Payload includes the full order object with final totals.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderCompletedEvent"
```

## OpenAPI 3.2 Features [#openapi-32-features]

OpenAPI 3.2 shipped in September 2025 and is current at 3.2.1 (released 2026-09-10). It introduces two features with direct agent relevance.

### Streaming Media Types [#streaming-media-types]

Native support for streaming response bodies, replacing custom content-type conventions:

```yaml
responses:
  "200":
    content:
      application/jsonl:
        itemSchema:
          $ref: "#/components/schemas/StreamChunk"
```

### `$self` for Multi-Document Specs [#self-for-multi-document-specs]

`$self` allows a spec to reference its own root document by identity, enabling cleaner multi-document compositions without fragile relative path references. This matters for large APIs split across multiple spec files that get merged for agent consumption.

## Full Well-Formed Operation Example [#full-well-formed-operation-example]

```yaml
paths:
  /invoices/{invoice_id}/send:
    post:
      operationId: send_invoice
      summary: Send an invoice to the customer by email
      description: >
        Sends a finalized invoice to the customer's email address on file.
        Call this after the invoice has been approved and is in "finalized"
        status. For draft invoices, call finalize_invoice first.

        Use resend_invoice if the customer reports not receiving the original.
        Use void_invoice to cancel a sent invoice.

        Returns the updated invoice with sent_at timestamp and delivery
        status. Returns 409 if the invoice is already sent or voided.
        Returns 422 if the customer has no email address on record.
      parameters:
        - name: invoice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: ID of the invoice to send
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                message:
                  type: string
                  maxLength: 500
                  description: >
                    Optional personal message to include in the email body.
                    Leave empty to use the default template text.
                cc:
                  type: array
                  items:
                    type: string
                    format: email
                  maxItems: 5
                  description: Additional email addresses to CC on the invoice email
      responses:
        "200":
          description: Invoice sent successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invoice"
              example:
                id: "inv_01HV3K8MNPQR"
                status: "sent"
                sent_at: "2024-03-15T14:22:00Z"
                customer_email: "billing@acme.com"
        "404":
          description: Invoice not found
        "409":
          description: Invoice is already sent or has been voided
        "422":
          description: Customer account has no email address on record
```

## Checklist [#checklist]

Before treating a spec as agent-ready:

* [ ] Every operation has a unique `operationId` in verb\_noun format
* [ ] Every operation description answers when to call it and when not to
* [ ] Similar operations explicitly disambiguate from each other
* [ ] All enum values are listed with semantic descriptions
* [ ] No operation has more than 10 parameters
* [ ] All error responses are documented with status codes
* [ ] Descriptions fall inside the [Length Guidelines](/docs/tool-design/naming-and-descriptions#length-guidelines) ranges
* [ ] Response schemas include a concrete `example`
* [ ] `format` keywords are used on string fields wherever applicable

## Publishing SDKs and Removing Onboarding Friction [#publishing-sdks-and-removing-onboarding-friction]

A well-formed spec still leaves an agent stuck if reaching a working call requires a human.

* Publish an official JavaScript/TypeScript SDK to npm, generated from the OpenAPI spec. Set `repository` and `homepage` in `package.json` so an agent verifying the package can trace it back to your API.
* For broader coverage, generate and publish SDKs for the other language ecosystems your users work in (Python/PyPI, Go modules, Ruby/RubyGems) from the same spec rather than maintaining them by hand.
* Link every published SDK from your API documentation and from `llms.txt`, so an agent that finds the docs finds the install command in the same pass. See [llms.txt](/docs/discovery/llms-txt).
* Minimize the steps that need a person: self-serve API keys, a sandbox or test-mode key that works without a sales call, and no mandatory approval gate before a first successful call. See [Test-Mode Keys and Sandbox Environments](/docs/authentication/api-keys#test-mode-keys-and-sandbox-environments) for the key-prefix and seeded-data conventions that make this self-serve.

Ora's catalog names these checks `npm-sdk-package` (an npm-published JS/TS SDK with `repository` and `homepage` set), `rest-sdk-packages` (SDKs across npm, PyPI, Go modules, and RubyGems, generated from the OpenAPI spec), and `onboarding-friction` (whether an agent can go from discovery to a first successful API call with no human in the loop - free tier or trial, self-serve keys, sandbox environment). See [Ora's checks catalog](https://ora.ai/api/checks).

## Related Pages [#related-pages]

* [Naming and Descriptions](/docs/tool-design/naming-and-descriptions) - the framework-agnostic rules for writing descriptions as prompts, disambiguation, and length that this page applies to OpenAPI fields
* [Tool Definitions](./tool-definitions) - how these OpenAPI operations become agent tool definitions
* [Arazzo Workflows](./arazzo-workflows) - composing operations into multi-step workflows
* [OpenAPI Extensions](./openapi-extensions) - semantic extensions for agent behavior hints
