Skip to main content

Queries

This documentation site is powered by GraphQL. Every page you are reading is also a row of data: the Markdown that renders this site is loaded into a live, typed GraphQL API and served back as queryable Doc records. You can ask the docs questions — "which pages are in the graphql section?", "search for idempotent", "give me the heading outline of the queries page" — and get back exactly the fields you select.

The API is read-only and open. There is no API key, no Authorization header, and no mutations. Just send the GraphQL envelope — a query string and an optional variables object — as JSON.

Every query goes to a single endpoint, over POST with a JSON body:

POST http://localhost:4000/graphql

Run the server with npm run graphql, then open http://localhost:4000/graphql for the GraphiQL explorer (introspection is on). The port is configurable with $PORT (default 4000). The examples below use localhost:4000 — substitute your own host if you deploy it elsewhere.


The query catalog

These are the root query fields of the schema (type Query):

FieldArgumentsReturns
metaApiMeta!
sections[Section!]!
docssection: String, search: String, limit: Int = 100[Doc!]!
docid: ID, slug: StringDoc
searchquery: String!, limit: Int = 20[Doc!]!

A response mirrors exactly the fields you select — nothing more. Ask for the fields you need, parse the shape you asked for. The full type definitions are in Schema overview.


meta — what's in here

meta returns a single ApiMeta describing the API: its name, a one-line description, the total docCount, the list of sections, and the endpoint path. It is the cheapest call to confirm the server is up and see the shape of the corpus:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query { meta { name docCount sections endpoint } }"}'
{
"data": {
"meta": {
"name": "Keen Docs GraphQL API",
"docCount": 49,
"sections": ["chat", "graphql", "keen-extension", "onboarding", "organization", "root", "tooling"],
"endpoint": "/graphql"
}
}
}

Add description to the selection for the longer blurb. meta takes no arguments.


sections — the table of contents

sections returns one Section per top-level area of the docs, each with a name, a docCount, and a docs list. Select just name and docCount for a compact table of contents:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query { sections { name docCount } }"}'
{
"data": {
"sections": [
{ "name": "chat", "docCount": 7 },
{ "name": "graphql", "docCount": 6 },
{ "name": "keen-extension", "docCount": 15 },
{ "name": "onboarding", "docCount": 10 },
{ "name": "organization", "docCount": 9 },
{ "name": "root", "docCount": 1 },
{ "name": "tooling", "docCount": 1 }
]
}
}

Each Section also exposes its full docs list, so you can fan out from a section straight into its pages — sections { name docs { id title } } — in one round trip. Keep nested selections shallow; very deep queries are rejected by the depth limit (see Errors and limits).


docs — list and filter

docs is the workhorse list query. With no arguments it returns every page (capped at limit, default 100). Three optional arguments narrow it:

  • section: String — restrict to one section (e.g. "graphql", "chat").
  • search: String — keep only pages whose title, description, or body contains the term (case-insensitive substring match).
  • limit: Int = 100 — cap the number of results returned.

The arguments combine. Here they are passed as variablessection and search together, capped at limit: 3:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query Docs($section: String, $search: String, $limit: Int) { docs(section: $section, search: $search, limit: $limit) { id title slug } }","variables":{"section":"graphql","search":"introspection","limit":3}}'
{
"data": {
"docs": [
{ "id": "graphql/agents", "title": "Drive the docs from an agent", "slug": "/docs/graphql/agents" },
{ "id": "graphql/errors-and-limits", "title": "Errors and limits", "slug": "/docs/graphql/errors-and-limits" },
{ "id": "graphql/index", "title": "GraphQL API", "slug": "/docs/graphql" }
]
}
}

Drop search to list a whole section — docs(section: "chat") returns all seven chat pages. Drop section to search the whole corpus. The Doc type carries far more than id/title/slug: description, section, category, wordCount, headings, and the full body. Select only the slices you need.


doc — fetch one page

doc returns a single Doc (or null). Look it up by either its stable id (the path under docs/ without extension, e.g. "chat/conversation") or its site slug (e.g. "/docs/graphql/queries"). This example fetches by id and pulls the heading outline along with metadata:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query Doc($id: ID!) { doc(id: $id) { id title slug section wordCount headings { depth text anchor } } }","variables":{"id":"chat/conversation"}}'
{
"data": {
"doc": {
"id": "chat/conversation",
"title": "Conversation",
"slug": "/docs/chat/conversation",
"section": "chat",
"wordCount": 712,
"headings": [
{ "depth": 1, "text": "Conversation", "anchor": "conversation" },
{ "depth": 2, "text": "URL shape", "anchor": "url-shape" },
{ "depth": 2, "text": "What's on the page", "anchor": "what-s-on-the-page" }
]
}
}
}

The headings array above is abbreviated — chat/conversation has eight headings in total. Each anchor is a slugified, deep-linkable id: lowercased, with every run of non-alphanumeric characters collapsed to a single -. To fetch by URL instead, pass slug:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query { doc(slug: \"/docs/graphql/queries\") { id title section } }"}'
{
"data": {
"doc": {
"id": "graphql/queries",
"title": "Queries",
"section": "graphql"
}
}
}

Pass id, slug, or both; a request matching nothing resolves to "doc": null. Add body to the selection to retrieve the full Markdown of a page (frontmatter stripped) — ideal for feeding one page into an agent's context.


search is the discovery query. It scans every page's title, description, and body for the query term (required) and returns matching Docs, capped at limit (default 20). Use it when you do not already know a page's id:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query Search($query: String!, $limit: Int) { search(query: $query, limit: $limit) { id title slug section } }","variables":{"query":"introspection","limit":20}}'
{
"data": {
"search": [
{ "id": "graphql/agents", "title": "Drive the docs from an agent", "slug": "/docs/graphql/agents", "section": "graphql" },
{ "id": "graphql/errors-and-limits", "title": "Errors and limits", "slug": "/docs/graphql/errors-and-limits", "section": "graphql" },
{ "id": "graphql/index", "title": "GraphQL API", "slug": "/docs/graphql", "section": "graphql" },
{ "id": "graphql/queries", "title": "Queries", "slug": "/docs/graphql/queries", "section": "graphql" },
{ "id": "graphql/running-the-api", "title": "Running the API", "slug": "/docs/graphql/running-the-api", "section": "graphql" },
{ "id": "graphql/schema-overview", "title": "Schema overview", "slug": "/docs/graphql/schema-overview", "section": "graphql" }
]
}
}

A narrower term shrinks the result set — searching for "idempotent" returns just the pages that mention it:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query { search(query: \"idempotent\") { id title } }"}'
{
"data": {
"search": [
{ "id": "graphql/errors-and-limits", "title": "Errors and limits" },
{ "id": "graphql/queries", "title": "Queries" }
]
}
}

search and docs(search:) use the same matcher; the difference is that search is search-first (the term is required) while docs is list-first (the term is one optional filter among several). Result sets reflect the live corpus, so exact counts shift as pages are added or edited.


Precise field selection

Because the response shape is your selection set, an agent fetches only what it needs in one round trip. The same doc call can be a one-field lookup or a full-page pull:

# Minimal: just confirm a page exists and get its URL.
query { doc(id: "graphql/queries") { slug } }

# Rich: pull everything for an agent's working context.
query {
doc(id: "graphql/queries") {
title
description
section
wordCount
headings { depth text anchor }
body
}
}

Select narrowly by default and widen only when you need the body. Deeply nested or oversized selections are bounded by the server's limits.


Response envelope

Every query returns the standard GraphQL envelope. On success:

{ "data": { "…": "…" } }

On failure, an errors array, each entry carrying a machine-readable extensions.code. Querying a field that does not exist on a type fails validation:

curl -sS http://localhost:4000/graphql \
-H 'content-type: application/json' \
--data '{"query":"query { doc(id: \"graphql/index\") { id nope } }"}'
{
"errors": [
{
"message": "Cannot query field \"nope\" on type \"Doc\".",
"locations": [{ "line": 1, "column": 39 }],
"extensions": { "code": "GRAPHQL_VALIDATION_FAILED" }
}
]
}

Branch on extensions.code rather than parsing the message text. The endpoint is guarded by graphql-armor with bounded query complexity — maxDepth 10, maxAliases 20, maxTokens 1500, and a cost limit of 8000 — so keep selections shallow and focused. The full reference is in Errors and limits.