# Output JSON Schema — Per Endpoint Entry

The exact structure every endpoint entry must follow. Copy field names exactly — no additions, no omissions, no renaming.

This is the schema used in `Step 3: Build the endpoint entry` of `SKILL.md`.

---

## Full example

```json
{
  "action_id": "list_users",           // ← from api_coverage.json
  "label": "List Users",               // ← from api_coverage.json
  "method": "GET",                     // ← from api_coverage.json
  "path": "/users",                    // ← from api_coverage.json
  "resource_url": "DOCS_BASE_URL/api/users#list",  // ← from api_coverage.json
  "category": "users",                 // ← from api_coverage.json
  "tags": ["users"],                   // ← from api_coverage.json — never empty (Pass 1 falls back to [category])
  "base_url": "API_BASE_URL/v2",       // ← fetched from docs page
  "description": "Returns the compact user records for all users in the workspace.",  // ← provider's documented description, VERBATIM (null if provider documents none)
  "_action_summary": "List all users in a workspace. Use when: building a people picker, syncing the user directory. Do NOT use for: fetching one user by ID (use get_user). Side effects: none — read-only.",  // ← LLM-optimized layer (purpose / use when / gotchas)
  "investigated_at": "<ISO timestamp>",       // ← set at time of fetch
  "auth_methods": [
    {
      "type": "bearer",
      "description": "Provider-issued opaque value carried in the standard auth header"
    }
  ],
  "required_scopes": ["read:users"],  // [] if no scope info found; only names from scope_list
  "headers": [
    {
      "name": "Authorization",
      "type": "string",
      "required": true,
      "transport": true,   // auth/runtime-managed — recorded for evidence, never an input (B4 skips it)
      "description": "Standard auth header",
      "possible_values": ["Bearer <opaque-value>"]
    },
    {
      "name": "Content-Type",
      "type": "string",
      "required": true,
      "transport": true,   // fixed value the caller never varies — build writes it as a literal step arg
      "description": "Request body format",
      "possible_values": ["application/json"]
    }
  ],
  "path_params": [
    {
      "name": "task_gid",
      "type": "string",
      "required": true,
      "description": "Globally unique identifier for the task",
      "usage_notes": null,
      "possible_values": [],
      "example": "321654",
      "pattern": null,
      "min": null,
      "max": null
    }
  ],
  "query_params": [
    {
      "name": "opt_fields",
      "type": "array",
      "required": false,
      "description": "Defines fields to return. Some requests return compact representations of objects in order to conserve resources and complete the request more efficiently.",  // provider verbatim
      "usage_notes": "Comma-separated. Nested fields use dot notation (assignee.name). Omitting it returns the compact record only.",  // LLM-optimized layer, null if nothing to add
      "possible_values": ["assignee", "assignee.name", "created_at", "created_by", "text"],
      "example": null,
      "pattern": null,
      "min": null,
      "max": null,
      "items": { "type": "string", "possible_values": [] }
    },
    {
      "name": "limit",
      "type": "number",
      "required": false,
      "description": "Number of results to return per page",
      "usage_notes": null,
      "possible_values": [],
      "example": 20,
      "pattern": null,
      "min": 1,
      "max": 100
    },
    {
      "name": "filter",
      "type": "object",
      "required": false,
      "description": "Filter conditions",
      "usage_notes": null,
      "possible_values": [],
      "example": null,
      "pattern": null,
      "min": null,
      "max": null,
      "properties": [
        {
          "name": "status",
          "type": "enum",
          "required": false,
          "description": "Filter by status",
          "usage_notes": null,
          "possible_values": ["active", "inactive", "deleted"],
          "example": null,
          "pattern": null,
          "min": null,
          "max": null
        },
        {
          "name": "created_after",
          "type": "string",
          "required": false,
          "description": "Return records created after this date",
          "usage_notes": "Format: ISO 8601 (e.g. 2024-01-01T00:00:00Z). Soft format hint from docs — no strict regex documented, so pattern stays null.",
          "possible_values": [],
          "example": null,
          "pattern": null,
          "min": null,
          "max": null
        }
      ]
    }
  ],
  "body_params": [
    {
      "name": "data",
      "type": "object",
      "required": true,
      "description": "Root data object",
      "usage_notes": null,
      "possible_values": [],
      "example": null,
      "pattern": null,
      "min": null,
      "max": null,
      "properties": [
        {
          "name": "type",
          "type": "enum",
          "required": true,
          "description": "Resource type",
          "usage_notes": null,
          "possible_values": ["user"],
          "example": null,
          "pattern": null,
          "min": null,
          "max": null
        },
        {
          "name": "attributes",
          "type": "object",
          "required": true,
          "description": "User attributes",
          "usage_notes": null,
          "possible_values": [],
          "example": null,
          "pattern": null,
          "min": null,
          "max": null,
          "properties": [
            {
              "name": "name",
              "type": "string",
              "required": true,
              "description": "Full name of the user",
              "usage_notes": null,
              "possible_values": [],
              "example": null,
              "pattern": null,
              "min": 1,
              "max": 255
            },
            {
              "name": "email",
              "type": "string",
              "required": true,
              "description": "Email address of the user",
              "usage_notes": "Must be unique across the workspace — provider returns 409 on duplicate.",
              "possible_values": [],
              "example": null,
              "pattern": null,
              "min": null,
              "max": null
            },
            {
              "name": "roles",
              "type": "array",
              "required": false,
              "description": "List of roles assigned to the user",
              "usage_notes": null,
              "possible_values": [],
              "example": null,
              "pattern": null,
              "min": null,
              "max": null,
              "items": {
                "type": "string",
                "possible_values": ["admin", "author", "learner", "integrationAdmin"]
              }
            }
          ]
        }
      ]
    }
  ],
  "error_codes": [
    {
      "status": 401,
      "reason": "unauthorized",
      "description": "Auth header missing or rejected by provider",
      "error_envelope_path": "$.error.message",
      "retry_strategy": "none"
    },
    {
      "status": 429,
      "reason": "rate_limited",
      "description": "Too many requests for this user",
      "error_envelope_path": "$.error.message",
      "retry_strategy": "wait_for_retry_after_header"
    }
  ],
  "pagination": {
    "style": "cursor",
    "page_size_param": "limit",
    "page_size_default": 50,
    "page_size_max": 100,
    "cursor_request_param": "after",
    "cursor_response_path": "$.next_cursor",
    "total_count_path": null,
    "loop_until": "response omits next_cursor"
  }
}
```

---

## Field-by-field rules

### Fields carried over from `api_coverage.json` (do NOT re-fetch or re-derive)

| Field | Source | Rule |
|-------|--------|------|
| `action_id` | `api.action_id` | Use exactly as-is — snake_case (e.g. `list_messages`). NEVER use the provider's `operationId` (e.g. `gmail.users.messages.list`) |
| `label` | `api.label` | Use exactly as-is |
| `method` | `api.method` | Use exactly as-is |
| `path` | `api.path` | Use exactly as-is |
| `resource_url` | `api.resource_url` | Use exactly as-is |
| `category` | `api.category` | Use exactly as-is |
| `tags` | `api.tags` | Use exactly as-is — always a non-empty array (Pass 1 guarantees at least `[category]`); never re-derive or drop |

### Fields fetched from the docs page

| Field | Rule |
|-------|------|
| `base_url` | Provider base URL (no path, no trailing slash) |
| `description` | Provider's documented description of the action, **verbatim** (trim markup/whitespace only). `null` if the provider documents none — never invent |
| `path_params` | All `{param}` segments in the path |
| `query_params` | All URL query parameters |
| `body_params` | All request body fields (key name is `body_params`, NOT `request_body`) |
| `headers` | ALL documented headers, classified: `transport: true` on auth/runtime-managed ones (`Authorization`, `Date`/signature dates, `Content-Length`, `Host`, fixed version pins) and on fixed-value pins the caller never varies; behavioral headers (`x-ms-lease-id`, `If-Match`) are ordinary parameters under their literal wire names |
| `auth_methods` | Auth types accepted by this specific endpoint |
| `required_scopes` | Scopes from `scope_list` (loaded once by the executor via `load_scopes.js`) required to call this endpoint |
| `error_codes` | HTTP status codes this endpoint can return |
| `pagination` | Pagination style (`null` if endpoint does not paginate) |

### Per-param description fields (two-field model)

Every param object (`path_params` / `query_params` / `body_params`, at every nesting depth) carries both fields below. `headers` are exempt — they only need `name/type/required/description/possible_values` (add `usage_notes` only when there's a real gotcha):

| Field | Content | Rule |
|-------|---------|------|
| `description` | Provider's documented description, **verbatim** | `null` if the provider documents none — never invent or rewrite |
| `usage_notes` | LLM-optimized layer: how the API uses it, constraints, gotchas | `null` when there's nothing beyond the provider text. See `references/extraction-rules.md → Description quality` |

### Optional entry-level fields (LLM-optimized summary + coverage self-check)

These keys are populated by Step 2 (extraction) and Step 2.5 (self-check). Absent = key not present in output; otherwise use the shape below.

| Field | When to include | Shape |
|-------|-----------------|-------|
| `_action_summary` | Whenever the action's purpose is known (from docs or evident from the endpoint). This is the LLM-optimized companion to the verbatim `description`. See `references/extraction-rules.md → Description quality` for the four-line template. | `string` — imperative, structured as `<purpose>. Use when: <...>. Do NOT use for: <...>. Side effects: <...>.` |
| `_coverage_notes` | Only when the coverage self-check found candidate param names that were **not** extracted and each has a defensible reason. Empty list = omit the key entirely. | `[{ "name": "<candidate>", "reason": "<one line>" }, ...]` |
| `skip_reason` | Only on stub entries. `"no_resource_url"` if `api.resource_url` was null; `"soft_404"` if Step 1.5 could not find a real docs page. | `"no_resource_url"` \| `"soft_404"` |

**Recursion rule reminder:** every param with `type: object` MUST carry `properties[]`. Every param with `type: array` whose `items.type === "object"` MUST carry `items.properties[]`. This applies at every depth, not just the top level. See `references/extraction-rules.md → Recursive object expansion` for the worked example.
