Skip to content

Notebooks

A Markdown document in your workspace is a notebook: ordinary prose with embedded blocks that render their live output inline. It’s the narrative counterpart to a frame’s page — use it for runbooks, incident write-ups, and annotated dashboards where explanation and data belong together.

Embedding a block

Add a fenced code block with the block language and a single @block/* definition (the same shape you’d author on a frame’s page):

# Checkout health
Total requests:
```block
{
"@block/stat": {
"title": "Requests",
"value": { "@expr/query": { "from": "requests", "select": { "value": "count()" } } }
}
}
```

The block’s visual output is injected directly after the fence. A document can hold any number of blocks interleaved with prose.

Rendering against the nearest frame

A block renders in the context of the nearest frame — the deepest frame in the document’s route chain. So a notebook at services/{ServiceName}/notes.md resolves its blocks against the services/{ServiceName} frame: their @expr/query, view columns, and filters all resolve there, exactly as on that frame’s page. A notebook with no surrounding frame still renders; blocks that need a frame’s data surface their own error.

Pinning a frame and a time window

A block fence can carry metadata after the language word, as key="value" attributes — handy for binding a block to a specific frame or a fixed window regardless of where the notebook lives:

```block title="Checkout requests" frame="services/checkout" from="now-1h" to="now"
{
"@block/stat": {
"value": { "@expr/query": { "from": "requests", "select": { "value": "count()" } } }
}
}
```
  • frame binds the block to that frame instead of the nearest one, so it renders against the frame’s view and scope. It is the frame’s concrete instance id — exactly what the address bar shows after @frames/, including any percent-escapes (services/checkout/operations/oteldemo.CheckoutService%2FPlaceOrder), not a {Param} pattern. Copy it from the URL, or pick the frame from the cell header.
  • from / to pin a time window — ISO timestamps (2026-06-01T00:00:00Z) or date math (now-1h, now) — independent of any surrounding range. from alone pins a window; to defaults to now.
  • title sets the label shown in the cell header in place of the block type.

All four are optional. The fence body stays just the inner @block/* definition; the frame and window are applied around it.

Embedding a SQL query

For an ad-hoc query, a sql fence is shorthand for a @block/table over the query — the fence body is the SQL, and the result renders as a table directly below it:

Slowest endpoints:
```sql
SELECT SpanName, quantile(0.99)(Duration) AS p99
FROM spans
GROUP BY SpanName
ORDER BY p99 DESC
```

A sql fence takes the same frame, from, to, and title attributes as a block fence, with the same meaning — so the query resolves against the bound frame’s tables and pinned window:

```sql frame="services/checkout" from="now-1h" to="now" title="Slowest endpoints"
SELECT SpanName, quantile(0.99)(Duration) AS p99 FROM spans GROUP BY SpanName
```

This is equivalent to a block fence wrapping { "@block/table": { "from": { "query": "…" }, "height": "auto" } } — the tagged { query } from lets it page through a large result in the database rather than fetching every row. Reach for the full block form when you need table columns, sorting, row clicks, or any block other than a table.

Every table in a cell — sql or block fence — sizes to the rows it returns: a cell’s result sits in the document’s flow, so a table never reserves a full page’s height the way it does in a dashboard slot (content_height is ignored here).

How a notebook renders

When you open a notebook by its route, it renders read-only: prose as Markdown, and each embedded block as a collapsible source above its live output. Blocks render through the same engine as a frame’s page, so their output updates as the underlying data changes.

Cells load as you reach them: a cell runs nothing — not its query, not its frame= binding — until it scrolls into view, so opening a long notebook costs only the cells on screen. Once a cell has run it stays loaded, so scrolling back past it doesn’t re-query. Prose is unaffected and always renders. A render with no viewport to scroll (noemata run, noemata validate --online) runs every cell up front instead.

Inspect mode (the toolbar toggle, or ⌘I) works here as on a frame’s page: hover a block to highlight it, click for its actions. A cell currently offers Copy screenshot — the other actions need a block’s authored source location, which a cell does not yet resolve.

Where notebooks live

A notebook under @frames/ is a versioned workspace file — author, validate, and commit it like any frame. You can also keep scratch notebooks in the app’s side panel; those are stored under .local/notebooks/ and are never version-controlled (the .local convention). Each scratch notebook is its own folder named with a human-readable slug, holding an index.md — the folder leaves room for sibling files (attachments, exports) to live alongside the document. The side panel gives each an editable title, sorted most-recently-edited first.

A scratch notebook has no surrounding frame, so its query cells run against a standalone runtime: raw SQL over real tables — and inline @expr/query over them — works, without a frame’s view columns, scalars, filters, or automatic time-scoping. Pin a frame attribute on a cell to render it against that frame instead.

A notebook is just Markdown — a versioned .md file under @frames/, or a .local/notebooks/<slug>/index.md for a scratch one — so you (or a coding agent) can create and edit one directly in the workspace.