Skip to content

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:

subtypeWhat it isDeclares inputs?
fragmentA composable piece, meant to be referenced by another document.Rarely — a composing document declares them.
templateA 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

frontmatter
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: terse

Each 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

typeFor
textFree text, one value.
numberA numeric value.
booleanTrue or false.
selectOne choice from a declared set.
multiselectAny number of choices from a declared set.
file-uploadOne or more uploaded files.
sortA declared set the user puts in order.
associatePairs drawn from two sets.

Modifiers

KeyApplies toMeaning
descriptionallWhat the value is for. Shown to whoever supplies it.
labelallA human-readable name for the field. since v0.16.0
defaultallUsed when nothing is bound.
requiredallThe caller must supply a value — see Validating a binding. A declared default satisfies it.
optionsselect, multiselect, sort, associateThe declared set. For sort the order you write is the initial ranking; for associate it is the left-hand set.
associate_withassociateThe right-hand set to pair against.
acceptfile-uploadAccepted file types.
max_size_bytesfile-uploadPer-file size ceiling.
max_filesfile-uploadHow 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~}
Readsanything in the execution context — whatever the caller suppliedonly what this document declared in its own inputs
Use it forcontext that comes from outside the documenta value the document’s own contract promises to accept
An undeclared namereads whatever happens to be there, or nothingcannot happen — only declared names are reachable
summarizer.exons
---
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:

text
{~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:

main.go
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:

main.go
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

DeclarationReported when
required: truethe 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 optionsa supplied value is not one of the declared options.
file-upload with max_filesmore files were supplied than the declaration allows.
file-upload with max_size_bytesa 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:

ValueRenders as
A string, number or booleanItself.
A listIts elements joined as prose. since v0.20.0 Set the separator with join=", ".
An objectIts readable fields, rather than a Go-shaped dump.
A list of objectsEach entry rendered in turn.
An uploaded file’s bytesNever 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:

09-typed-inputs-report.exons
---
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