Generated docs
Every input in a contract has a description, and may have details. The app's developers write both where they already document a setting, in a doc comment beside the code that reads it, and the SDK exports them into contract.cue. One generator, docuconf docs in the CLI, turns the contract into documentation for every language: CONFIG.md for developers and CONFIG.agents.md for AI agents. SDKs export the text; they do not render it.
Description and details
| Field | Rule |
|---|---|
description | Required on every variable and file input. Plain text, at least 5 characters: what the input is, in one sentence or phrase. Renderers keep it on one line. |
details | Optional. CommonMark: why the input exists and when to change it. Not blank, and at most 4000 characters, counted in Unicode code points. Used only in generated docs, never at runtime. |
Each SDK takes both from the language's natural doc location: usually the first paragraph of the doc comment is the description and the rest is the details. Language-specific doc syntax (Javadoc tags, XML doc elements, Go doc links, Doxygen commands) is converted to CommonMark or dropped. Export fails when a description is missing or too short, or when details are blank or too long. In the Go example, WORKER_COUNT is documented like this:
// Number of background workers processing orders.
//
// Each worker holds one database connection, so keep it below the
// database's connection limit divided by the number of replicas.
// Raise it when the order queue grows faster than it drains.
WorkerCount int `env:"WORKER_COUNT" envDefault:"4" min:"1" max:"64"`WORKER_COUNT: {
type: "int"
description: "Number of background workers processing orders"
details: "Each worker holds one database connection, so keep it below the database's connection limit divided by the number of replicas. Raise it when the order queue grows faster than it drains."
default: 4
min: 1
max: 64
}In the generated docs, details keep their Markdown, with one change: their headings are demoted to nest under the input's own heading, so they cannot break the document's outline.
The pipeline
contract.cue | contract.json ──build──▶ docs model (docs.json) ──render──▶ CONFIG.md (developers)
└─render──▶ CONFIG.agents.md (AI agents)- Contract. The app's SDK exports
contract.cue, with each input's description and details. - Docs model. The CLI unifies the contract with the meta-schema, so that defaults such as
required: falseand a list'sencodingare explicit, and builds the docs model: a versioned JSON document (kind: ConfigDocs,apiVersion: docs.docuconf.dev/v1alpha1) that holds every fact a renderer shows, already phrased. Each input's type label, wire format, values-file form, constraints, allowed sources and possible boot errors are in it, and every model is checked against#DocsModel. A secret input never has a default, an example or a profile value in the model, whatever the contract held. - Render. A renderer reads only the model, so a website, an MCP server or a Backstage plugin can render the same facts in the same words. The output is deterministic: the same model always gives the same bytes.
CONFIG.mdis the reference for developers: a table of contents by group, then a section per input with its description, a table (type, required, secret, default, constraints, wire format, values-file form, config key, path, sources, boot errors), its schema, examples and details; then profiles and overlays when the contract has them, what each source means, and each boot error's meaning and fix.CONFIG.agents.mdis for AI agents: coding agents working in the app's repository, and agents that set deployment values. It starts with hard rules: never put a secret value in code, a.envfile, a values file, a ConfigMap or an annotation; use each input's wire format; validate withdocuconf vetordocuconf checkbefore proposing a change; never invent an input the contract lacks. Then how to use the config in code, onekey: valueblock per input, and the boot errors. It can be included in anAGENTS.mdor served as anllms.txt-style file.
See them for the Go orders example: CONFIG.md and CONFIG.agents.md.
The command
docuconf docs <contract.cue | contract.json | docs.json> [--format model|markdown|agents] [-o file | --check file]| Flag | Meaning |
|---|---|
--format markdown | The reference for developers, such as CONFIG.md. The default. |
--format agents | The rules and per-input facts for AI agents, such as CONFIG.agents.md. |
--format model | The docs model itself, such as docs.json. |
-o file | Write to the file instead of standard output. |
--check file | Write nothing: exit 0 when the file is what would be generated, otherwise exit 1 with a diff. Cannot be combined with -o. |
-contract file | The contract, if not given as the argument. |
The input is a contract, in CUE or as JSON, or a docs model, recognised by its apiVersion. Commit the docs next to contract.cue and keep them current in CI:
docuconf docs contract.cue -o CONFIG.md
docuconf docs contract.cue --format agents -o CONFIG.agents.md
docuconf docs contract.cue --check CONFIG.md
docuconf docs contract.cue --format agents --check CONFIG.agents.mdWhere each SDK takes them from
From each SDK's README:
| SDK | description | details |
|---|---|---|
| Go | The first paragraph of the field's doc comment, on one line, without its final period; a desc tag is the fallback | The rest of the doc comment: headings (# Heading), lists and indented code carry over as Markdown |
| TypeScript (T3 Env) | .describe(), or .meta({ description }) | The TSDoc comment above the variable, read at export; or .meta({ details }), or annotate() for other validators |
| TypeScript (NestJS) | @Describe("...") | The TSDoc comment above the property and its decorators, read at export; or @Details("...") |
| .NET | [Description] (or [Display(Description = ...)]), else the XML doc <summary> | The XML doc <remarks>, from the documentation file the project generates (GenerateDocumentationFile) |
| Python | Field(description=...), else the first paragraph of the attribute docstring | The attribute docstring (the rest of it, without a description), or Field(json_schema_extra={"details": ...}) |
| Ruby | describe :attr, "..." | The YARD comment directly above describe, or its details: option |
| Java (Spring Boot) | The first sentence of the property's Javadoc (the record component's @param, the field or the getter); @Description overrides it | The rest of the Javadoc, read by the annotation processor at compile time |
| Kotlin | @Doc("..."), else the first sentence of the property's KDoc | @Doc(details = "..."), else the rest of the KDoc, indexed by the dev.docuconf Gradle plugin |
| Rust | The first paragraph of the field's /// doc comment, or #[docuconf(description = "...")] | The rest of the doc comment, or #[docuconf(details = "...")] |
| Swift | The first paragraph of the @Env description argument, which may be a whole multi-line doc comment: a property wrapper cannot read /// | The rest of that text, or .details("...") |
| Elixir | The first paragraph of the @doc above env, or description: | The rest of the @doc, or details: |
| Gleam | The description argument of each builder, such as docuconf.int(name, description) | details (file_details for a file); Gleam cannot read a comment at run time |
| C++ | The description argument of add_var or add_file, .description(), or the first paragraph of a Doxygen comment given to .doc() | The rest of the .doc() comment, or .details("...") |
| PHP (Laravel) | The description argument of Env::int(...) and the other helpers, else the first paragraph of the PHPDoc comment above the config entry | The rest of the PHPDoc comment, or details: |
| PHP (Symfony) | The description key of each variable in docuconf.yaml | The details key |
| COBOL | The first paragraph of the comment above the copybook field, or @desc | The rest of the comment, or @details |
The normative text is SPEC.md §14.