Skip to content

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.