.agents/skills/improve-oas/SKILL.md
Add descriptions, examples, and code samples to a Kibana API area so that the generated OpenAPI specification is complete and useful for humans and AI agents.
This skill operates on any package or plugin in the Kibana repo. It detects how the plugin generates its OAS contribution and adapts accordingly. For issue categorization, use debug-oas.
debug-oas to get the current issue list and baseline count.Do not skip steps 1-2 unless the developer already provided the API area and path.
Determine whether the plugin uses code-first or spec-first OAS, and which schema library it uses. These two axes drive every other decision.
| Approach | How to detect | Source of truth |
|---|---|---|
| Code-first | No *.gen.ts files; schemas defined in TypeScript | Route code and TypeScript schemas |
| Spec-first | *.gen.ts files present; @kbn/openapi-generator in package.json or build scripts | YAML specs (usually under docs/openapi/ or common/api/) |
Most Kibana plugins are code-first. Spec-first is used primarily by Security solution plugins/packages (security_solution, entity_store, cases, osquery, elastic-assistant, and others). When in doubt, check for *.gen.ts files — their presence is definitive.
Code-first rules:
oasOperationObject (see Examples section below).docs/openapi/ that is merged into the final published spec. If present, check whether the plugin puts its OAS prose in route code, in the YAML, or both, and update accordingly.Spec-first rules:
*.gen.ts files — they are overwritten on regeneration.description fields flow through to generated TypeScript via JSDoc.example/examples fields do not flow to generated code — they are documentation-only artifacts in the YAML and bundled output.yarn openapi in the plugin directory) to regenerate.For code-first plugins, the schema library determines how to add field descriptions:
| Library | How to detect | Field descriptions |
|---|---|---|
@kbn/config-schema | Imports from '@kbn/config-schema' | meta: { description: '...' } on schema calls |
| Zod | Imports from '@kbn/zod' or '@kbn/zod/v4' | .meta({ description: '...' }) on schema calls |
For spec-first plugins, this doesn't apply — field descriptions go in the YAML specs regardless of what the generated code uses.
Beyond the two axes above, plugins vary in how they organize code: schemas in one large file vs inline per route, descriptions in constants files vs inline strings, versioned vs non-versioned router, createServerRoute() vs direct router calls, etc.
Always match the existing style of the plugin you are working in. Read a few existing routes and schema files to understand the local conventions before making changes. If descriptions are centralized in constants, add to the constants. If schemas are inline, keep them inline. Do not impose a different organizational pattern.
docs/openapi/ detectionCheck whether the plugin has a docs/openapi/ directory. If it does, list all path YAML files and example YAML files — these are part of the published spec and must be updated alongside route code (see the docs/openapi/ section under Phase 3).
Run debug-oas for the API area to get the current issue list. Record the baseline issue count.
Then, for each public route in the plugin:
summary, description, and schema metadata already exist.docs/openapi/, oasOperationObject references, or inline.Work route-by-route. For each route, bundle all improvements together: schema field descriptions, route summary/description, examples, and code samples. This keeps changes reviewable and focused.
These rules apply regardless of the plugin's OAS approach.
Verify against implementation. Read the handler function and trace how each parameter is used before writing a description. Never infer behavior from parameter names alone. Do not expose implementation details in the description — see target audience rules below.
Flag unused parameters. If a parameter is accepted in the schema but never referenced in the handler body, flag it to the developer rather than documenting nonexistent behavior.
Summary format: 5-45 characters, start with a verb, sentence case, no trailing period, include articles. Match the operation verb — for example, "Get a data view" on a GET and "Delete a data view" on a DELETE. Path parameters shared across methods must have context-appropriate descriptions too.
Description format: Explain what the operation does and why a user would call it. Document constraints and side effects visible to the API caller, and relationships to other API operations. Use markdown links for references to external docs. End descriptions with a newline character in YAML.
Target audience. The reader is an API consumer — a developer integrating with Kibana from the outside, or an AI agent calling the API. They do not have access to Kibana source code and do not know internal implementation details. Descriptions should answer "what does this do for me?" not "how is this implemented?"
@kbn/config-schema field descriptionsAdd meta: { description } to every schema.*() call that represents an API-visible field:
// Before
name: schema.string({ maxLength: 1000 }),
// After
name: schema.string({
maxLength: 1000,
meta: { description: 'A display name for the data view.' },
}),
For fields with no existing options object:
// Before
override: schema.maybe(schema.boolean()),
// After
override: schema.maybe(
schema.boolean({
meta: {
description:
'Override an existing data view if a data view with the provided identifier already exists.',
},
})
),
When schema.maybe() wraps another schema call, place meta on the inner call, not on schema.maybe() itself.
When a field is used in both request and response schemas, write a single description that works for both contexts. If the meaning differs by context, use separate schema definitions.
If the same schema appears in multiple routes, versions, or status codes, give it an ID via meta.id. This lets the OAS generator emit a $ref to #/components/schemas/<id> instead of inlining the full schema every time.
With @kbn/config-schema:
export const addressSchema = schema.object(
{ street: schema.string(), city: schema.string() },
{ meta: { id: 'Address', description: 'Mailing address for a foo resource.' } }
);
With Zod:
export const addressSchema = z
.object({ street: z.string(), city: z.string() })
.meta({ id: 'Address', description: 'Mailing address for a foo resource.' });
If an API uses tagged unions, prefer schema.discriminatedUnion('type', [...]) or z.discriminatedUnion('type', [...]) over a plain union. With Zod, when each variant has its own .meta({ id }), the OAS generator produces a discriminator mapping automatically.
For plugins using Zod schemas, use .meta({ description }):
// Before
name: z.string(),
// After
name: z.string().meta({ description: 'A display name for the stream.' }),
Important: .meta() is immutable — it returns a new schema. Always attach .meta() at definition time. Calling it later does not update the schema that your route already references.
Every route needs a summary and description. Read existing routes in the plugin to see if and where these are already set. If some routes already have them, follow that pattern. If none do, add them directly to the route config — on the top-level config for versioned routes, or in the options object for non-versioned routes and createServerRoute.
If the plugin centralizes descriptions in a constants file (e.g. *_DESCRIPTION exports), add or update entries there rather than inlining.
For OAS to include response body documentation, the route must define validate.response with a schema and description for each status code. If a route is missing response validation, the generated OAS will have no response schema — add one.
Always define response schemas as factory functions so the schema object is not allocated in production — response validation is only used at dev time for OAS generation:
// Correct: schema is only instantiated when called
const myResponseSchema = () => schema.object({ id: schema.string(), name: schema.string() });
// Wrong: schema is eagerly allocated at module load
const myResponseSchema = schema.object({ id: schema.string(), name: schema.string() });
Use these standard response descriptions for consistency across plugins:
| Status | Description |
|---|---|
| 200 | Indicates a successful call. |
| 400 | Bad request. |
| 401 | Authorization information is missing or invalid. |
| 403 | Insufficient privileges. |
| 404 | Object is not found. |
Code-first plugins provide examples via oasOperationObject — a lazy function on the route options that returns either an inline object or a path to a YAML file. The result is deep-merged into the auto-generated OAS operation. Check existing routes in the plugin to see which style is already in use.
Prefer inline TypeScript objects for new examples — they are type-checked at dev time. Use YAML files only when the plugin already uses them or the examples are large. If adding YAML files, place them in an examples/ directory alongside the route files and name them {noun}_{verb}.yaml (e.g. agents_create.yaml).
A typical oasOperationObject includes request/response examples and code samples. Use examples (plural map with named keys), not example (singular):
requestBody:
content:
application/json:
examples:
createExample:
summary: Create a weekly maintenance window
value:
title: "Weekly patching window"
schedule:
custom:
starts_at: "2025-03-12T02:00:00.000Z"
duration: "1h"
responses:
200:
content:
application/json:
examples:
createResponseExample:
summary: A created maintenance window
value:
id: "a1245-678-abc"
title: "Weekly patching window"
x-codeSamples:
- lang: curl
source: |
curl \
-X POST "${KIBANA_URL}/api/maintenance_window" \
-H "Authorization: ApiKey ${API_KEY}" \
-H "kbn-xsrf: true" \
-H "Content-Type: application/json" \
-d '{"title":"Weekly patching window"}'
- lang: Console
source: |
POST kbn://api/maintenance_window
{"title":"Weekly patching window"}
Example quality rules:
null, not omitted."string" or 0. Prefer middle-range values for numeric fields.examples:.Code sample conventions:
curl and Console samples.curl from Console syntax using @elastic/request-converter rather than hand-writing. Apply fixups: replace $ELASTICSEARCH_URL with ${KIBANA_URL}, replace $ELASTIC_API_KEY with ${API_KEY}, and add -H "kbn-xsrf: true" for mutating methods (POST, PUT, DELETE).x-codeSamples (camelCase) for new code sample keys, unless the plugin already uses x-code-samples (hyphenated) — in that case, match the existing key name.docs/openapi/ path and example filesIf the plugin has docs/openapi/ path YAML files, you MUST update them. These files are merged into the published API spec and are the primary place where x-codeSamples, description, and examples appear for consumers. Skipping them means the published docs will be incomplete.
For each path YAML file: ensure summary and description match the route-level constants, and add x-codeSamples with both curl and Console samples. For each example YAML file: ensure a description field is present.
If x-codeSamples cannot be added via oasOperationObject due to typing limitations, add them to the YAML path files instead. Do not skip code samples.
After modifying YAML files, regenerate bundled output:
cd <plugin_path>/docs/openapi && npx @redocly/cli bundle 2>/dev/null
For deprecated routes, set deprecated: true on the route config and prefix the description with a deprecation notice. Use one of these forms:
Deprecated in <version>. Use <replacement endpoint> instead. — for routes with a direct replacement.Deprecated in <version>. — when no replacement exists./api/index_patterns mirroring /api/data_views), use the primary route's description prefixed with the deprecation notice.Before moving to verification, confirm every item below is complete. Do not proceed with partial coverage — but also do not fabricate descriptions to check boxes. If a field or route's behavior is unclear after reading the handler, flag it to the developer rather than guessing.
summary and description set.meta: { description } (or .describe() for Zod).docs/openapi/ path YAML file has description and x-codeSamples for each operation.docs/openapi/ example YAML file has a description field.bundled.yaml, bundled.json) has been regenerated if any YAML was modified.validate-oas skill for the API area to confirm the issue count dropped. It handles environment refresh and scoped validation.docs/openapi/ with bundled files, regenerate bundled output.debug-oas if any issues remain to understand what is left.schema.maybe() placement: Place meta: { description } on the inner schema call, not on the schema.maybe() wrapper.createCasesRoute). The underlying config shape is the same — look at what the wrapper passes through.operationId typos: Fix these opportunistically when spotted in YAML path files. Search the codebase for references to the old operationId before renaming — generated clients derive method names from it, and the workflows/connector pipeline maintains an explicit allowlist of operation IDs.meta.id (or .meta({ id }) for Zod) to the shared schema definition so the generator emits a $ref instead..meta() immutability: .meta() returns a new schema. If you call .meta() after the schema is already referenced by a route, the route keeps the old schema without your metadata. Always attach .meta() at definition time.@kbn/config-schema types (schema.byteSize(), schema.duration(), schema.any(), schema.conditional(), etc.) produce poor or lossy OAS. See dev_docs/tutorials/generating_oas_for_http_apis.mdx for the full compatibility table and preferred alternatives.availability: { since, stability } to generate x-state annotations in the OAS. If a route is missing this metadata, flag it to the developer — the values require knowledge of release history.