Per-row templates for the data rune
Give {% data %} an optional body, transformed once per row with $row bound, so a data file can drive arbitrary Markdoc rather than only table cells.
Give {% data %} an optional body, transformed once per row with $row bound, so a data file can drive arbitrary Markdoc rather than only table cells.
f117249f3e18d5babdbe98319ed3a5fdc1945feb60{% data %} accepts an optional body, transformed once per row with $row bound{% accordion-item %} tags inside {% accordion %} is the case to pin$item is not accepted as an aliasroot, orient, columns, where, sort, limit, offset) apply identically with or without a bodycollection templatesProjectFiles seam<table> is required (inside chart / datatable) is a clear build error, not an empty rendernumeric / text with a body present — warn or silently ignore/runes/data documents it with a worked example, cross-referenced from collection's per-item templatesSplice the rows in; do not wrap them. preprocessData currently does a 1:1 replacement (node.children[i] = resolveDataToNode(...)), so emitting N rows forces this line to change. Wrapping them in one container to preserve the 1:1 shape is the tempting minimal edit, and its effect depends on which container: a spike hand-authoring {% accordion-item %} at each depth found direct children and a {% div %} wrapper both fine, while {% section %} and {% grid %} wrappers destroyed the items and their body text with no error or warning. See SPEC-127 for the table. Splicing costs one line and makes the question moot for every parent rune.
Note the spike probed only the destination half — {% data %} has no body form to test until this work item ships, so the end-to-end composition is an acceptance criterion here, not an established fact.
Stay shallow. Bind $row, render the body, no control flow. Formatting goes through the shared markdoc functions, not template syntax. Add conditionals and iteration and this becomes Handlebars with different delimiters — collection's templates avoid that deliberately and this inherits the constraint.
$row, not $item, and no alias. The shapes differ: a collection $item is an entity (id / type / url / data), a data row is flat. One name for two shapes is a trap, and an alias would make the wrong mental model work just often enough to be believed. See SPEC-127 for the full argument.
columns keeps meaning select-and-rename with a body present — it determines which keys $row exposes and under what names. numeric / text exist to emit data-value for charts and sorting, so they are arguably meaningless without a table; decide whether they warn or are ignored, and say so in the docs either way.
Nesting — a row whose value is itself an array — is explicitly out of scope. It is the first step toward the control flow this is trying not to build.
Completed: 2026-09-10
Branch: claude/content-author-docs-org-vps1un
packages/runes/src/data-pipeline.ts — resolveData returns Node[], and walkAndReplaceData splices rather than assigning. bindRow deep-clones the body per row, substituting Variables whose path starts with row; rowObjects projects the typed table into plain objects keyed by column header. A TABLE_CONSUMERS check makes a body inside chart / datatable a build error.
packages/runes/src/tags/data.ts — a note on why the content model stays empty while a body must still be allowed: the body never reaches the transform, but Markdoc rejects the tag before the preprocessor sees it otherwise.
packages/runes/test/data-row-template.test.ts — 9 tests, including the one SPEC-127 could not write before: {% accordion %} building two items from generated {% accordion-item %}s.
Docs — /runes/data gains the body form, the composition example, and the limits; /runes/collection cross-references it and says why the binding name differs.
$row binds at preprocess, not via config.variables — the binding differs per row and one shared config cannot express that. Variables reach the AST as Variable objects in a node's attributes, on tags and text nodes alike, so one rule covers both.numeric / text warn with a body rather than being silently ignored — they type data-value on cells a body does not emit.card moves href onto the link element it wraps the surface in, not the card tag. The binding was correct.