Control flow
Most blocks are static structure. These let the block tree depend on data — repeat a block per row, or pick a block by value — so a page can adapt to what the query returns, or hold part of it back until the reader gets there.
@block/for
Repeats a child block once per item of each. Reach for it to turn a query result into a list of blocks: a card per service, a row per alert. Inside the child, item is the current element and index its position, readable with @expr/get_context or in a @expr/handlebars template. Rename them with as / index_as to disambiguate nested loops. Pair it with @block/pagination when the list is long.
{ "@block/for": { "each": { "@expr/get_context": "rows" }, "block": { "@block/text": { "text": { "@expr/handlebars": "{{index}}: {{item.name}}" } } } }}@block/case
Renders the one case that matches a context value — the block-level equivalent of a switch statement. Use it to swap a whole view by state: a status badge per health level, a different chart per selected mode. The top-level key is the context value to match on; default is the fallback.
{ "@block/case": { "status": { "ok": { "@block/badge": "Healthy" }, "error": { "@block/badge": "Down" }, "default": { "@block/text": "Unknown" } } }}To let the reader pick which case shows, pair it with a @block/switch segmented control that writes the same context key.
@block/lazy
Holds its child back until the reader scrolls it into view. Nothing inside is reduced before that — no expression compiles, no query runs — so a long page only pays for the part someone actually looks at. Reach for it on pages that stack many independent queries down a scroll: a notebook, a long report, a hub with a dozen panels below the fold.
The child goes inline (or under block, like any other single-child block). placeholder is what stands in until it mounts.
{ "@block/lazy": { "placeholder": { "@block/text": "Scroll to load" }, "@block/table": { "from": { "query": "SELECT service, p95 FROM latency" } } }}Once mounted the child stays mounted — scrolling back past it doesn’t tear down its state or re-run its queries.
An omitted placeholder leaves the slot empty, which collapses to zero height. Several empty lazy blocks in a row therefore all sit inside the first screenful and mount together, which defeats the point — give each one a placeholder roughly the size of its child.
Deferring needs a viewport to report the child on screen. A render with none — noemata run, noemata validate --online — mounts every lazy child up front instead, so validation and screenshots see the whole page rather than a stack of placeholders.