Fill rules on a slot's `fill:`
A slot's `fill:` declaration in `section.ts` — the decision that gives it a value, what a fill pass reads, and how to write its briefs.
A slot declares how it is filled beside everything else about it: the decision that
gives it a value sits in the same builder call as its label, its budget and its bounds.
The decision’s id and the slot it produces are the slot’s own key, so neither is
written by hand and neither can drift out of agreement with the slot it fills.
import { defineSchema, image, imageAssign, text, textBlock } from "@homepages/template-kit";
export const schema = defineSchema({ label: "Hero", slots: { hero_image: image({ label: "Hero image", fill: imageAssign({ pick: "The most striking exterior shot of the property." }), }), blurb: text.long({ label: "Blurb", cap: 400, fill: textBlock({ structure: "paragraph", perspective: "third", voice: "Confident premium-listing voice. Warm and concrete, favoring specifics over adjectives. Never a specific checkable claim the facts do not support.", content: "What makes this property worth a visit, in two or three sentences under 400 characters.", }), }), },});One origin per slot
Section titled “One origin per slot”Every top-level slot answers where its value comes from exactly once. A slot bound with
source: — to a fact or to a compute() — is baked from that binding, so also giving it
a fill: is a compile error; a slot declaring neither is refused when the section builds,
because nothing would ever write it. The binding-only kinds take no fill: at all: no
decision produces a single number, url or select value, so a source is the only
origin they have.
Two slots are exempt from naming one, deliberately rather than by omission. A row field
inside a collection may declare neither, because the collection’s own row origination
writes it. And a standalone poi may declare neither, because its origin is the
editor’s place search — its builder takes no source: and no fill: in the first place.
Which builder a slot’s fill: accepts is decided by the slot’s kind, and each builder
takes only its own kind’s options — one pointed at the wrong slot is a compile error
rather than something check finds later. The fill decision types
is the full set.
A collection’s row fields are the one place a fill: sits one level down. Where the rows
themselves come from is the collection slot’s own declaration — fill: chooseFrom({ count, pick })
when the model originates them, and nothing at all when a source: binding does, since
one row per entry of the bound facts array is the binding. Each authored row field then
carries its own fill:, and a field the row origination already writes carries none.
On a source-bound collection, a filled field is written onto the baked row.
The binding bakes the rows first — one per entry, each source:-bound field
carrying the projection’s value — and the fill pass then writes the authored
fields onto those same rows. What lands is the enriched array, so a field you
gave a fill: is owned by the fill pass on every row, while the baked
projection supplies the source:-bound fields and the row members no
declaration names at all. That is why the two never need reconciling by hand,
and why a row field declares exactly one of the two origins.
What a fill pass reads
Section titled “What a fill pass reads”A fill pass hands the model a slice of the property rather than the whole record, so that a decision sees what it needs and not the rest of the listing. That slice always carries the address and coordinates, the property type and listing intent, the units, the narrative and the distilled document notes, and the primary contact’s own scalars. A section holding any image decision also gets the property’s whole photo pool.
reads on defineSchema is how a section widens the slice, and the vocabulary is three
keys: tours, floor_plans and photos.
import { defineSchema, text, textBlock } from "@homepages/template-kit";
export const schema = defineSchema({ label: "Layout", reads: ["floor_plans"], slots: { layout_note: text.long({ label: "Layout note", cap: 300, fill: textBlock({ structure: "paragraph", perspective: "third", voice: "Plain and factual. Never a specific checkable claim the facts do not support.", content: "How the home is laid out floor by floor, under 300 characters.", }), }), },});Declare a key when the copy has to be grounded in that bucket: a paragraph about the
layout cannot be written from the always-on slice however the brief is worded. Naming
photos is how a section with no image decision still receives the pool, which is
what lets its text decisions read the per-photo tags and captions.
Omitting reads is not a degraded mode — most sections write from the always-on slice
and declare nothing. And it governs the model’s view alone: a source:-bound slot, fact
binding or compute() alike, is baked from the full property record, so it never needs a
reads entry for the bucket it reads.
cap is the slot’s, and a decision inherits it
Section titled “cap is the slot’s, and a decision inherits it”The character budget belongs to the text slot — its cap in
the schema — and a text-block filling it states none. That one
number is what the editor’s field permits a user typing into the slot, and what the
model is asked to write to; a slot filled by a decision must declare it, and check
rejects one that does not. A generated list reads its per-item budget the same way,
off the list slot’s element.
A decision may state a cap of its own only to ask for something strictly
shorter — a headline generated at 48 into a slot that holds 60. A value at or above
the slot’s is rejected by check: above, because the overrun would be trimmed away
invisibly; equal, because it restates a number already declared and can only drift
from it.
The cap reaches the model twice — as a hard maxLength on the response schema, and
as a number in the brief. What it does not do is constrain the stored value: no
check rejects a longer one and nothing downstream re-measures it, so the cap governs
what is asked for and what will be cut.
A value over cap is retried once with a stricter instruction. If the retry is still over, the platform trims it — at the last sentence boundary that fits inside the cap, or hard at the cap when there is no sentence boundary to fall back to. The value is never dropped and never lands over cap, so an overrun surfaces as copy that ends early, not as a fill that failed.
That makes the brief, not the cap, the thing to get right. Restate the character
budget in the content brief. A brief that asks only for a sentence count (“in 2–4
sentences”) overruns a tight cap on a property with a lot to say, and the trim is
what the reader gets. The scaffolded skeleton’s headline states both — “in one
sentence, under 80 characters” against a slot cap of 80.
count is what fill writes; min/max is what the editor allows
Section titled “count is what fill writes; min/max is what the editor allows”A generated list carries two different numbers, and they are not redundant.
count sits on the decision and is the generation target — how many entries
a fill pass produces. min/max sit on the list slot and are the editing
bounds — how far a user may shrink or grow the list afterwards in the editor.
features: list.of(text.short({ label: "Feature", cap: 80 }), { label: "Features", max: 12, // the editor lets a user grow the list to twelve fill: listFill({ count: 6, // a fill pass writes six structureHint: "Short concrete noun phrases. No sentences, no duplicates.", voice: "Concrete and specific, never vague praise.", }),}),Filling to the editing maximum would leave a user nothing to add and no signal about how many entries the design actually wants, so the two are declared separately, each where it belongs: the target on the decision that writes the value, the bounds on the slot that holds it.
Writing a voice brief
Section titled “Writing a voice brief”voice is the tone every answer a textBlock produces is written in; content is the
extra instruction for this one slot. The house style for voice is three parts, in this
order — the register, the qualities, then what the copy may not do:
“Confident premium-listing voice. Warm and concrete, favoring specifics over adjectives. Never a specific checkable claim the facts do not support.”
The register (confident premium-listing voice) is who is speaking; the qualities (warm and concrete, specifics over adjectives) are what to optimize for; the last part is the prohibition, and it is the one to write on every prose decision. A model handed a thin set of facts and an open brief closes the gap with plausible detail — a walk to a station that may not be there, a renovation nobody recorded — and nothing downstream can tell that sentence from a true one. The grounding clause is what stops it:
“…never a specific checkable claim the facts do not support.”
Neither has a shared home. There is no template-level voice and no kit-supplied default, so a section wanting one register across six slots writes it six times: each is a real decision about that slot’s copy, and reading one should never mean opening a second file to resolve what it inherits.
perspective is required on every textBlock, and names the grammatical person the copy
is written in — first for a voice speaking as the agent or developer, second to address
the buyer directly, third for the detached listing register most sections use. There is
no omitted mode and no default to look up: every prose fill says which person its copy is
in.
Writing a pick brief
Section titled “Writing a pick brief”A decision that carries a pick hands the model every candidate as
{ id, tags, caption } (a video candidate also carries duration_ms) and your prompt
as the brief. The tags come from intake’s
image and video classification — room:kitchen, category:exterior, composition:aerial,
vibe:bright and so on — and the caption is a short free-text description. So
the model can already reason about subject, room, framing and mood; your job is
to tell it which of those matter for this slot.
Name the subject, then the qualities, then the tie-breaker:
“The most visually striking exterior shot of the property — wide, well-lit, the building clearly the subject. Prefer a curb-appeal or twilight shot over an interior or a detail.”
Subject (exterior shot of the property), qualities (wide, well-lit, building is the subject), tie-breaker (prefer curb-appeal or twilight). A prompt with only a subject leaves the model guessing on a large pool; one with a tie-breaker stays stable as a property’s photo set grows.
See also
Section titled “See also”- The schema — the slot a decision
produces. - AI writes slot content — the decision types as a task-shaped delta.
fixtures.ts— what a decision-filled slot needs from a fixture.