Devlin here. @daloyjs/core@0.14.1 shipped to npm today and it adds one optional field to app.route() called meta. That single field carries structured examples, extra description copy, and a free-form extensions bag — all of it validated against your Standard Schema at build time and surfaced into the OpenAPI 3.1 document and a sibling routes.json via daloy inspect --ai. The whole release is additive and non-breaking: every existing route keeps working with zero changes. The 0.14.2 patch keeps that default JSON shape and adds --yaml / --format yamlfor the same dump when the reader is a human or an LLM context window. This is the "AI-friendly route metadata" milestone from the roadmap, and it is the last pre-1.0 milestone before the secure-by-default initiative takes over.
The failure mode this fixes
# What "the codegen agent guessed it" failure mode looks like.
#
# Prompt: "write a fetch call that creates a book"
#
# What the agent shipped, with full confidence:
#
# await fetch("/books", {
# method: "POST",
# headers: { "content-type": "application/json" },
# body: JSON.stringify({ name: "Dune" }), // ← field is 'title', not 'name'
# });
# // Response shape it invented:
# // { bookId: string, name: string, createdAt: string }
# //
# // Real response shape:
# // { id: string, title: string }
#
# Why? The OpenAPI doc had a schema, but no examples. The agent read
# the schema, inferred plausible field names from the operationId
# ("createBook"), invented a 'createdAt' because every API has one,
# and shipped a typed client whose types and runtime disagreed.
#
# The schema was not wrong. The schema was just not the most
# pattern-matchable artifact in the room. A single concrete example
# would have anchored the agent to the real field names.
every codegen agent that ever read an OpenAPI doc · 0/10 starsTS · UTF-8 · LF
None of this is the agent's fault either. It read the schema. It picked the most plausible field names from theoperationId. It invented a createdAtbecause that's what most APIs return. The schema was right but not specific enough to anchor the model to the real field names. One concrete example would have changed every decision downstream.
What landed in 0.14.x
// The new optional 'meta' field on app.route(), in one place.
//
// Authored once, surfaced everywhere:
// - OpenAPI requestBody examples
// - OpenAPI response examples (per status code)
// - operation-level x-daloy-examples vendor extension
// - sibling routes.json via 'daloy inspect --ai'
// - validated against the route's Standard Schema at build time
//
// Existing routes keep working unchanged. meta is optional everywhere.
import { App } from "@daloyjs/core";
import { z } from "zod";
const app = new App();
const Book = z.object({
id: z.string(),
title: z.string(),
}).strict();
app.route({
method: "POST",
path: "/books",
operationId: "createBook",
tags: ["Books"],
request: { body: z.object({ title: z.string().min(1) }) },
responses: {
201: { description: "Created", body: Book },
400: { description: "Invalid" },
},
meta: {
description: "Create a book record.",
tags: ["AI"],
examples: {
happy: {
summary: "Standard create",
request: { body: { title: "Dune" } },
response: { status: 201, body: { id: "1", title: "Dune" } },
},
missingTitle: {
summary: "Validation failure",
request: { body: { title: "" } },
response: { status: 400 },
},
},
extensions: {
"x-codegen-hint": "books-table",
},
},
handler: async ({ body }) => ({
status: 201 as const,
body: { id: crypto.randomUUID(), title: body.title },
}),
});
one new optional field on route() · everything else unchangedTS · UTF-8 · LF
meta.examplesSurfaces: OpenAPI requestBody/response examples · routes.json · validated in CI
Named record of { summary?, description?, request?: { params?, query?, headers?, body? }, response?: { status, body?, headers? } } pairs. Every field is optional individually; pass only the parts you want documented. Both sides — request and response — are schema-checked at build time.
meta.description / meta.tagsSurfaces: OpenAPI operation description + tags
Augment the route-level fields of the same name. Route-level values win when both are set; tags are de-duplicated and concatenated, so you can keep transport tags on the route and audience tags (AI, Public, Internal) on the meta block.
meta.extensionsSurfaces: OpenAPI Operation Object x-* keys
Free-form bag emitted onto the OpenAPI operation. Keys without an x- prefix are prefixed automatically for spec compliance, so codegen-hint becomes x-codegen-hint. Daloy does not interpret these — it preserves them so your downstream tooling can.
Build-time validation, not vibes-based docs
# What 'pnpm daloy inspect --check' does to every meta.examples block.
#
# For every named example on every route, Daloy validates:
#
# request.params → against route.request.params (when both exist)
# request.query → against route.request.query
# request.headers → against route.request.headers
# request.body → against route.request.body
#
# response.body → against route.responses[example.response.status].body
# (unknown status code is itself an error)
#
# Any mismatch FAILS the contract run, which means:
#
# - The OpenAPI doc never publishes a sample that does not match the
# schema. The codegen agent cannot be misled by a stale example
# because the example cannot survive a stale schema in CI.
#
# - The example AND the schema are kept honest by the same gate.
# There is no "examples drift" surface to monitor; it is a build
# failure.
#
# CI gate (verbatim from the create-daloy template):
#
# $ pnpm daloy inspect --check
# ✓ 14 routes
# ✓ 38 examples validated
# ✓ no contract issues
#
# Bad example, same gate:
#
# $ pnpm daloy inspect --check
# ✗ POST /books · example 'happy' · response.body.id is required
# exit 1
schema and examples kept honest by the same gateTS · UTF-8 · LF
This is the single most important property of the feature. There is no "examples drift" surface to monitor, because a stale example fails the contract run before the OpenAPI doc is even published. The docs and the schema can never be out of sync with each other, because they are both gated by the same pnpm daloy inspect --check command — the one the scaffolded CLI inspector already runs in CI.
What lands in OpenAPI 3.1
{
"paths": {
"/books": {
"post": {
"operationId": "createBook",
"tags": ["Books", "AI"],
"x-codegen-hint": "books-table",
"x-daloy-examples": {
"happy": { "summary": "Standard create", "...": "..." },
"missingTitle": { "summary": "Validation failure", "...": "..." }
},
"requestBody": {
"content": {
"application/json": {
"schema": { "type": "object", "properties": { "title": { "type": "string" } } },
"examples": {
"happy": { "summary": "Standard create", "value": { "title": "Dune" } },
"missingTitle": { "summary": "Validation failure", "value": { "title": "" } }
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/Book" },
"examples": {
"happy": { "summary": "Standard create", "value": { "id": "1", "title": "Dune" } }
}
}
}
},
"400": { "description": "Invalid" }
}
}
}
}
}
Swagger UI, Scalar, and Hey API all read these without extra wiringTS · UTF-8 · LF
The named examples land on every relevant slot of the OpenAPI 3.1 spec — requestBody.content.*.examples, responses.*.content.*.examples, and the operation-level x-daloy-examples vendor extension for tools that want the full structured shape including the response status code. That last one is why the docstrings on the Hey API typed client start showing your real example values on the next pnpm gen run.
daloy inspect --ai dumps the whole catalog
# 'daloy inspect --ai' dumps the route catalog as a single document
# with JSON Schema for every input + output and every meta.examples
# entry. The format every codegen tool and LLM system-prompt
# scratchpad needs.
# Write to disk for Hey API / your codegen pipeline
$ pnpm daloy inspect --ai > routes.json
# Or emit YAML — typically ~30% smaller than the equivalent pretty JSON,
# which matters when the file ends up inside an LLM context window.
$ pnpm daloy inspect --ai --yaml > routes.yaml
$ pnpm daloy inspect --ai --format yaml > routes.yaml
# Pipe through jq to enumerate operationIds (JSON only)
$ pnpm daloy inspect --ai --json | jq '.routes[].operationId'
# Scope the dump with the usual filters
$ pnpm daloy inspect --ai --tag Books
$ pnpm daloy inspect --ai --method POST
# What an LLM system prompt looks like with this file inlined:
# "You are writing TypeScript fetch calls for the Books API.
# The full route catalog and validated examples are below.
# Use the operationId for naming, and never invent field names
# that are not in the request/response JSON Schemas."
# <routes.yaml>
flat JSON · no Daloy runtime coupling · feeds any codegenTS · UTF-8 · LF
{
"daloy": { "ai": 1 },
"generatedAt": "2026-05-19T12:00:00.000Z",
"routeCount": 1,
"routes": [
{
"method": "POST",
"path": "/books",
"operationId": "createBook",
"tags": ["Books", "AI"],
"request": { "body": { "type": "object", "properties": { "title": { "type": "string" } } } },
"responses": {
"201": { "description": "Created", "body": { "$ref": "#/schemas/Book" } },
"400": { "description": "Invalid" }
},
"examples": {
"happy": {
"summary": "Standard create",
"request": { "body": { "title": "Dune" } },
"response": { "status": 201, "body": { "id": "1", "title": "Dune" } }
}
},
"extensions": { "x-codegen-hint": "books-table" }
}
]
}
every route, every input schema, every output schema, every exampleTS · UTF-8 · LF
daloy:
ai: 1
generatedAt: "2026-05-19T12:00:00.000Z"
routeCount: 1
routes:
- method: POST
path: /books
operationId: createBook
tags:
- Books
- AI
request:
body:
type: object
properties:
title: { type: string }
required:
- title
responses:
"201":
description: Created
body:
type: object
properties:
id: { type: string }
title: { type: string }
"400":
description: Invalid
examples:
happy:
summary: Standard create
request:
body:
title: Dune
response:
status: 201
body:
id: "1"
title: Dune
extensions:
x-codegen-hint: books-table
same dump, ~30% fewer tokens · pass --yaml or --format yamlTS · UTF-8 · LF
The dump is intentionally a flat JSON file with no DaloyJS runtime coupling. Feed it to Hey API codegenas a sibling artifact so the generated SDK's docstrings carry your examples. Drop it into an LLM system prompt for "write me a fetch call that hits the books endpoint". Or pipe it into your own Python / Go / Postman generator — every field is plain JSON.
Why "schema + examples" beats "schema alone"
# Why "schema + examples" beats "schema alone" for codegen agents.
#
# The schema tells the agent the SHAPE of a valid payload.
# The example tells the agent what one ACTUALLY LOOKS LIKE.
#
# These are not redundant. They serve different mental operations:
#
# schema → type checker. Catches structural errors.
# example → pattern matcher. Catches semantic errors.
#
# A schema says: { id: string, title: string }.
# An example says: { id: "1", title: "Dune" } — and the agent now
# knows your ids look like short opaque strings, not UUIDs, not ints,
# not URL slugs. Every downstream call site picks up that signal.
#
# Multiply by the unhappy path:
#
# The 'missingTitle' example pins exactly which validation rule
# fires on an empty string. The agent generating a form-validation
# client now writes the right client-side guard FIRST, not after
# the user files a bug.
#
# Multiply by extensions:
#
# x-codegen-hint: "books-table" is a free-form lane for your own
# conventions. SDK builders, OpenAPI overlays, and prompt templates
# read it. Daloy does not interpret it — it just preserves it.
schema → type checker · example → pattern matcher · different jobsTS · UTF-8 · LF
The scope, on purpose
# What 'meta' deliberately is — and is not.
#
# It IS:
# - Optional per-route. Existing routes need zero changes.
# - A documentation + codegen-aid surface. Examples are validated,
# so they cannot lie about the schema, but they do NOT replace
# the schema. The schema remains the single source of truth.
# - Free-form on the 'extensions' lane. Keys without an 'x-' prefix
# are auto-prefixed for OpenAPI spec compliance.
# - Surface-stable across runtimes. The same code emits the same
# OpenAPI on Node, Bun, Deno, Workers, and Vercel Edge.
#
# It IS NOT:
# - A runtime mock. Examples are validated at build time and
# emitted into docs. They do not get returned by the handler.
# If you want a mock server, that is a separate tool reading
# 'routes.json' or the OpenAPI doc — not a Daloy feature.
# - A way to override the route schema. If 'response.status: 201'
# in your example does not exist in 'responses', that is a hard
# error — not a permissive cast.
# - Limited to JSON. The schema-aware validator runs against any
# Standard Schema (Zod, Valibot, ArkType, TypeBox); the example
# payload is whatever your schema accepts.
# - A new author surface. It is one optional field on the same
# 'route()' call you were already writing. No new file, no new
# concept, no new build step.
docs + codegen aid · NOT a runtime mock · NOT a schema overrideTS · UTF-8 · LF
Adopting it on an existing route
# Adopting it on an existing route, in 6 lines.
app.route({
method: "GET",
path: "/books/:id",
operationId: "getBook",
request: { params: z.object({ id: z.string() }) },
responses: {
200: { description: "ok", body: Book },
404: { description: "not found" },
},
+ meta: {
+ examples: {
+ happy: { request: { params: { id: "1" } },
+ response: { status: 200, body: { id: "1", title: "Dune" } } },
+ notFound: { request: { params: { id: "missing" } },
+ response: { status: 404 } },
+ },
+ },
handler: async (ctx) => { /* ... */ },
});
# Run the contract gate. The build either passes or names the bad
# example. Then 'pnpm gen' picks up the new examples on Hey API's
# next codegen pass — your typed client gets enriched docstrings
# for free.
#
$ pnpm daloy inspect --check
$ pnpm gen
6 added lines · happy + unhappy example · contract gate stays greenTS · UTF-8 · LF
Concretely, what changes for the agent
# Concretely: what changes for the codegen agent.
#
# Before (schema only):
# Agent reads OpenAPI schema for POST /books.
# Agent picks plausible field names from operationId.
# Agent invents a 'createdAt' because every API has one.
# Agent ships a typed client whose runtime doesn't match its types.
# PR review catches it. Maybe.
#
# After (schema + validated examples + routes.json):
# Agent reads OpenAPI schema AND the example { title: "Dune" }.
# Agent uses the exact field name 'title'.
# Agent reads the validated response example and knows ids are
# short strings, not UUIDs, and there is NO 'createdAt' field.
# Agent writes the test using the 'missingTitle' example for the
# 400 path, because that example told it which validation rule
# fires on empty input.
# PR review checks the handler, not the fetch call.
#
# Not theoretical. This is the diff I see on PRs where the agent
# had a routes.json in its context vs the ones where it didn't.
not theoretical · the diff I see in PRs where routes.json is in contextTS · UTF-8 · LF
The pre-flight checklist
# Pre-flight: is your route 'meta'-ready?
#
# 1) Every public route has at least one 'happy' example.
# [ ] request shape matches the schema
# [ ] response.status is a key in 'responses'
# [ ] 'pnpm daloy inspect --check' is green
#
# 2) State-changing routes also have at least one unhappy example.
# [ ] one 400/422 validation example
# [ ] one 401/403 if auth is required (no body needed)
# [ ] one 404 for resource-by-id routes
#
# 3) 'routes.json' is wired into a build artifact.
# [ ] pnpm script: "ai:dump": "daloy inspect --ai > routes.json"
# [ ] file is consumed by Hey API or an LLM system prompt
# [ ] CI re-runs the dump and fails on unchecked drift
#
# 4) Examples are kept honest by the contract gate.
# [ ] CI runs 'pnpm daloy inspect --check' on every PR
# [ ] no skip flag exists in the project
#
# 5) Extensions are intentional, not noisy.
# [ ] every x-* key is documented in the README
# [ ] no x-* key duplicates an OpenAPI standard field
five sections · paste into the README under 'Working with AI agents'TS · UTF-8 · LF
Wrapping up
The whole point of contract-first is that one route definition becomes your validation, your types, your OpenAPI doc, your typed client, and your contract tests. With meta, that same single route definition also becomes the structured context a codegen agent needs to write the call site correctly on the first try — and the validator that refuses to let any of those artifacts drift apart in CI. No new files, no new build step, no new vendor lock-in. Just one optional field on the call you were already writing.
@daloyjs/core@0.14.2 and create-daloy@0.8.2 are on npm now. New projects pick up the field and the YAML-friendly CLI flags automatically; existing projects can adopt them one route at a time without changing anything else.
Closest neighbors: the AI-friendly route metadata docs page for the full surface reference, the CLI inspector post for the contract gate that keeps the examples honest, the contract-first post for how the typed client picks the examples up, and the AGENTS.md + skills post for the prose-side context that pairs with this machine-readable one.
— Devlin