Inputs
Declaring typed inputs, the exons.input tag, and how a bound value renders into a prompt.
A document that is meant to be run usually needs values it does not contain: the text to summarise, how many bullet points, which tone. The inputs block is where a document declares those values, and {~exons.input~} is how its body reads them.
inputs is one of the frontmatter fields listed in the Format Reference. This page is its full vocabulary.
First: fragment or template? since v0.19.0
A prompt document declares which of two things it is:
subtype | What it is | Declares inputs? |
|---|---|---|
fragment | A composable piece, meant to be referenced by another document. | Rarely — a composing document declares them. |
template | A complete unit, meant to be executed with per-run values. | Yes. |
subtype applies to prompt documents only, and it is optional — a prompt that does not declare one is a fragment. If you are writing a fragment, a skill, or an agent, the rest of this page is background rather than something you need.
Declaring an input
inputs:
text:
type: text
description: The source material to summarise.
max_bullets:
type: number
default: 5
tone:
type: select
label: Tone of voice
options:
- value: neutral
- value: warm
- value: terseEach key is the input’s name. The order you write them in is preserved since v0.20.0 — a UI that builds a form from this schema asks the questions in the order you asked them.
Each entry under options is an object with a value — what gets stored and what the document sees — and an optional label, which is what a person is shown. Omit the label and consumers fall back to the value.
Input kinds since v0.19.0
type | For |
|---|---|
text | Free text, one value. |
number | A numeric value. |
boolean | True or false. |
select | One choice from a declared set. |
multiselect | Any number of choices from a declared set. |
file-upload | One or more uploaded files. |
sort | A declared set the user puts in order. |
associate | Pairs drawn from two sets. |
Modifiers
| Key | Applies to | Meaning |
|---|---|---|
description | all | What the value is for. Shown to whoever supplies it. |
label | all | A human-readable name for the field. since v0.16.0 |
default | all | Used when nothing is bound. |
required | all | The caller must supply a value — see Validating a binding. A declared default satisfies it. |
options | select, multiselect, sort, associate | The declared set. For sort the order you write is the initial ranking; for associate it is the left-hand set. |
associate_with | associate | The right-hand set to pair against. |
accept | file-upload | Accepted file types. |
max_size_bytes | file-upload | Per-file size ceiling. |
max_files | file-upload | How many files may be supplied. |
exons.input vs exons.var
Two tags, two jobs. exons.var was not replaced — it gained a sibling with a narrower one.
{~exons.var~} | {~exons.input~} | |
|---|---|---|
| Reads | anything in the execution context — whatever the caller supplied | only what this document declared in its own inputs |
| Use it for | context that comes from outside the document | a value the document’s own contract promises to accept |
| An undeclared name | reads whatever happens to be there, or nothing | cannot happen — only declared names are reachable |
---
name: summarizer
type: prompt
subtype: template
inputs:
text:
type: text
max_bullets:
type: number
default: 5
---
Summarize into at most {~exons.input name="max_bullets" /~} bullets.
{~exons.input name="text" /~}Declared inputs are injected under a reserved context root called input, with each default already applied since v0.21.0
. So {~exons.input name="max_bullets"~} is the path input.max_bullets — which means control flow needs no new syntax:
{~exons.if eval="input.verbose"~}Explain your reasoning.{~/exons.if~}
{~exons.for item="s" in="input.sources"~}- {~exons.var name="s" /~}{~/exons.for~}A declared input that is neither bound nor defaulted is present-but-empty: it renders as nothing, evaluates as false, and a loop over it runs zero times. That is deliberate. Because a name is reachable under input only if the frontmatter declared it, an absent name is a typo rather than an ambiguity.
exons.var is unchanged
{~exons.var~} still reads runtime context, exactly as it always has. Until exons.input existed since v0.21.0
both jobs — “read a value the caller passed” and “read a value this document declared” — carried the same name, because there was no way to tell them apart. Nothing an existing document does needs to change; the new tag lets you say something the old one could not distinguish.Binding values
A declared input is a contract with whoever runs the document. The caller passes a map, keyed by input name, to Execute:
output, err := tmpl.Execute(ctx, map[string]any{
"summary": "Checkout returned 500 for 6 minutes",
"severity": 2,
"sources": []string{"logs", "pager"},
})Nothing about that map is checked by Execute — a declared input that is neither bound nor defaulted simply renders as nothing. Requiredness and option membership are validated separately, by the caller, before the render:
for _, verr := range tmpl.Spec().ValidateInputBinding(values) {
// one error per offending input; decide whether to refuse or proceed
}That split is deliberate. A form wants to tell a person which field is wrong, and a renderer wants to produce text; folding the two together would mean a half-filled form could only ever fail as one opaque render error.
What ValidateInputBinding checks
| Declaration | Reported when |
|---|---|
required: true | the value is absent or empty and the input declares no default. A default satisfies requiredness — the value is never actually missing at render, so refusing would make an unsubmittable form. |
select, multiselect, sort with options | a supplied value is not one of the declared options. |
file-upload with max_files | more files were supplied than the declaration allows. |
file-upload with max_size_bytes | a file exceeds the per-file ceiling. |
Only constraints the document actually states are enforced. In particular the input-kind vocabulary stays open: an unrecognized type is not an error, because a document may have been written against a newer version of the library than the one reading it.
How a value renders
A bound value has to become text. The rules are the same whether it arrives through exons.var or exons.input:
| Value | Renders as |
|---|---|
| A string, number or boolean | Itself. |
| A list | Its elements joined as prose. since v0.20.0
Set the separator with join=", ". |
| An object | Its readable fields, rather than a Go-shaped dump. |
| A list of objects | Each entry rendered in turn. |
| An uploaded file’s bytes | Never as text. A file’s raw content is withheld and a description of the file appears instead. |
That last row is worth knowing about before you hit it. A file-upload value carries bytes, and bytes carry no evidence of whether they are prose or a PNG — so rendering them into a prompt is never the right guess. The file is described, not inlined. since v0.21.2
A worked example since v0.24.0
Everything above in one runnable document — five typed inputs, a required one, a select this child narrows from the parent it extends, and a for loop that degrades instead of failing when its list is not supplied:
---
name: incident-report
description: An incident report. Extends base-report and restates tone in its own voice.
subtype: template
version: 1.0.0
inputs:
tone:
type: select
description: An incident report is never casual, so this one narrows the parent's choice.
default: formal
options:
- value: formal
label: Formal
summary:
type: text
description: One line describing what happened.
required: true
severity:
type: number
description: 1 (worst) to 5 (cosmetic).
default: 3
sources:
type: multiselect
description: Optional. Where the evidence came from.
options:
- value: logs
label: Service logs
- value: traces
label: Distributed traces
- value: pager
label: Pager timeline
---
{~exons.extends template="base-report" /~}
{~exons.block name="heading"~}# Incident: {~exons.input name="summary" /~}{~/exons.block~}
{~exons.block name="body"~}
Severity {~exons.input name="severity" /~}.
Evidence:
{~exons.for item="src" in="input.sources"~}- {~exons.var name="src" /~}
{~/exons.for~}
Timeline:
{~exons.for item="row" in="input.timeline" onerror="default" default="_No timeline was supplied._"~}- {~exons.var name="row" /~}
{~/exons.for~}
{~/exons.block~}
It is byte-identical to go-exons/examples/09-typed-inputs/report.exons, which ships a main.go that binds values, validates them, and renders — go run . in that folder. The parent it extends is on the Examples page.
Next
- Error recourse —
onerror=, as used on the loop above. - Format Reference — every frontmatter field and template tag.
- Concept — why a document has two parts.