Data: records, collections & access
Wit declares structured data inline with #name: value, where the
value is a record { … }, a collection [ … ], or a scalar. Declare it once; a
self-organising document reads from it and renders it many ways. Reach into it
from prose with @name.field.
#world: { location - Bag End, time - night, storm - true }For the underlying value system — the six DataValue kinds, JSON mapping, conditions, and coercion — see Data model.
Records
A record is a set of key/value fields. The separator is a spaced hyphen - or a colon — both produce
the same field:
#point: { x - 3, y - 7 } #point: { x: 3, y: 7 }
A nested value omits
the separator entirely — the brace or bracket is the signal: { history { years - 31 } } and { tags [ a, b ] } (nested-record).
Fields are separated by a comma or a newline, never by bare whitespace, so inline and block records are identical:
#report: {
title: Q3 Review
quarter: 2024
final: true
}A trailing comma is tolerated (trailing-comma) and an empty { } is
legal (empty-record). Multi-word keys are stored verbatim — years at post (multi-word-key) — and multi-word values are trimmed
(multi-word-value).
The comma-in-value trap
An unquoted comma splits fields, because the parser reads it as a field boundary:
#cite: { author - Boud, D., year - 2024 } ~ E_MALFORMED_RECORD
That is E_MALFORMED_RECORD — "record field missing key". Quote the value:
#cite: { author: "Boud, D.", year: 2024 }A quoted string makes its bytes literal (commas, newlines become content) and
forces a string type; only " and \ are recognised
inside, and an unterminated quote is E_UNTERMINATED_STRING. A multi-line record value uses
the key: / indented-block shape, terminated by a top-level comma or
the closing brace (block-record-multi-line-value and its
-with-close / -with-comma-terminator siblings).
Collections
A collection is an ordered list in [ … ]. Items are separated by a comma or newline — not whitespace. That distinction trips
everyone once:
#a: [ x y z ] ~ ONE item, the string "x y z" #b: [ x, y, z ] ~ THREE items
Multi-word items are kept whole (multi-word-items); per-item typing is mixed
(mixed-types); collections may hold records inline (inline-records) or in blocks
(multi-line-records, multi-line-values) and may nest (nested-collection); a
trailing comma is tolerated (trailing-comma) and an empty [ ] is
legal (empty-collection).
Scalars are typed eagerly
Every bareword value is classified at parse time into one of four
types, and quoting forces a string. The type is not cosmetic — it drives
conditionals, truthiness, iteration, and how {{…}} coerces a
value. The rules, in order:
trueorfalse→ boolean.null→ null.- Matches
-?(?:[0-9]+|[0-9]+.[0-9]+)→ number. - Anything else → string.
The number pattern is strict: an optional leading minus, then
digits, optionally a single dot and more digits. There is noleading or trailing bare dot, no exponent, and nounary plus. So these all stay strings: .5, 5., 1e3, +1, and "2024" (quoted).
#report: { title - Q3 Review, quarter - 2024, final - true }title is a string, quarter a number, final a
boolean. The same happens per item in a
collection:
#mixed: [ 1, two, true ]Number, string, boolean respectively.
Classification is not limited to data definitions. Every non-flag parameter
value — through pipes, parens, record-arg, form-fill, or colon-scatter — is run
through the same probe, so a value passed by any form can arrive as a
typed number, record, or collection and be a valid (each) target, @x.field target, or (if) operand.
Conditionals compare by stringifying both sides, so a quoted "2024"and a bare 2024 do compare equal — both stringify to 2024. But "2024" is not a number: the distinction
matters wherever a real numeric type is expected, not for equality. See Conditionals for the comparison model.
Access: fields and indices
Data access reaches into a record or collection from prose: @name.field, @collection.0.field, and deeper
chains. It is the verified way to put data into a sentence — not {{…}}, which does not interpolate in prose. Follow the handle
with a dot and a field name; the chain reads segments until it hits a byte that
is not part of one — a sentence-final period ends it cleanly:
#keeper: { name - Ada } The keeper was @keeper.name.
The keeper was Ada.
Collections index by zero-based number:
#findings: [ { claim - A, strength - high }, { claim - B, strength - low } ] First: @findings.0.claim. Second strength: @findings.1.strength.
First: A. Second strength: low.
Chains go as deep as the data: @keeper.history.years (access-into-nested-record) and @x.y.z.w (deep-chain).
Fuzzy key matching
Keys match by canonical form: lowercase the key, then keep only [a-z0-9] — spaces, hyphens, and underscores drop out, and camelCase
collapses. So a field stored as years at post is reachable by any
space-free spelling:
#keeper: { years at post - 31 } camel @keeper.yearsAtPost, snake @keeper.years_at_post
camel 31, snake 31
Both resolve to 31. If two different keys collapse to the same canonical form,
the access is E_AMBIGUOUS_RECORD_KEY.
A space ends the segment
A space terminates the access segment.So a multi-word key written with spaces does not resolve:
#keeper: { years at post - 31 } He served @keeper.years at post here.
Only @keeper.years is read — it is unresolved, and at post trails as prose. Multi-word
keys are reachable only through the space-free camel or snake forms above.
Missing keys degrade, not fail
A missing field is not a parse error — it is resolved at expand
time and rendered as an unresolved marker
(<span class="wit-unresolved">), so a typo shows up in the output
rather than breaking the build.
The value-block form
A complex value that spans lines or contains nodes is wrapped as a value-block
def: #name: … !!. This is why some data
definitions end in !! — the block form lets the value hold markup
a single line could not.
Common mistakes
- Leaving an unquoted comma in a value (it splits fields).
- Expecting
[ a b c ]to be three items — it is one. - Expecting
.5,1e3, or+1to be numbers — the strict pattern leaves them strings. - Assuming
"2024"is a number — it is a string that happens to compare equal to2024. - Typing spaces into an access, like
@keeper.years at post— use the camel or snake form. - Expecting a missing field to fail the build — it degrades gracefully at expand time.
- Reaching for
{{…}}to read data in prose — use@field.
See also
Data model· Conditionals · Iteration · Record-arg (same grammar) · Interpolation & captures · Escapes & quoting