Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Kcup Format

A kcup is a single TOML file named *.kcup.toml. Four parts: identity, gatherers, prompt, output schema.

Top-level fields

FieldTypeRequiredDescription
namestringyesIdentifier shown in logs and as the output title.
descriptionstringnoOne-line summary printed on each run.

[[gather]] blocks

Zero or more gather blocks run before the model call. Each captures one piece of context and binds it to an id you can interpolate into the prompt.

FieldTypeRequiredDescription
idstringyesThe handle used in the prompt as {{id}}.
shellstringone ofShell command run via sh -c. Captures stdout, trimmed.
filestringone ofFile path read as UTF-8.
optionalbooleannoIf true and file cannot be read, the value is empty instead of failing.

Exactly one of shell or file must be set per block. A shell gatherer fails the run if the command exits non-zero (its stderr is shown). A file gatherer fails if the file is missing, unless optional = true.

[prompt] block

FieldTypeRequiredDescription
systemstringyesSystem message — sets behavior, voice, output rules.
userstringyesUser message template. {{id}} placeholders are replaced with gathered values before the call.

Interpolation is plain string replace. Unknown placeholders are left as-is.

[output] block

FieldTypeRequiredDescription
schematableyesMap of field name → JSON-schema type. Drives structured output across all providers and post-call validation.

Supported schema types

The validator recognizes these strings (from src/output.rs):

TypeValidator check
stringJSON string
numberJSON number (int or float)
integerJSON integer (signed or unsigned 64-bit)
booleanJSON boolean
arrayJSON array (element types are not constrained)
objectJSON object (nested keys are not constrained)

Any other type string is passed through to the provider untouched but the local validator will accept any value for it. Stick to the six above unless you know what you're doing.

Every field in schema is treated as required. The model is asked to return all of them.

Minimal example

name = "echo"
description = "Round-trip a single string through the model."

[prompt]
system = "You repeat the user's message back, lowercased."
user = "Hello, world!"

[output]
schema = { result = "string" }

No gatherers, one input, one output field.

Maximal example

name = "release-notes"
description = "Draft release notes from the diff and merged PRs since the last tag."

[[gather]]
id = "last_tag"
shell = "git describe --tags --abbrev=0"

[[gather]]
id = "commits"
shell = "git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:'%h %s'"

[[gather]]
id = "diff_stat"
shell = "git diff --stat $(git describe --tags --abbrev=0)..HEAD"

[[gather]]
id = "changelog"
file = "CHANGELOG.md"
optional = true

[prompt]
system = """
You write release notes in the style of crates.io changelogs.
Group changes into: Features, Fixes, Internal. Be specific. Cite commit shas.
"""
user = """
Last tag: {{last_tag}}

Commits since:
{{commits}}

Diff stat:
{{diff_stat}}

Existing CHANGELOG (for tone reference):
{{changelog}}
"""

[output]
schema = { version = "string", features = "array", fixes = "array", internal = "array", summary = "string" }

Four gatherers (one optional file), a multi-line prompt, and a richer schema.