Skip to content

Published data

Every build publishes each node’s output rows into the Workspace through Fontana Files, alongside that port’s columns, its audit items, its lineage and its data styles. Everything that shows you a run’s data reads it from there: Explore, the port grids on the canvas, Data tiles on a dashboard, and the templates in Markdown tiles, Stat tiles and node descriptions.

Two things follow from that. Data exists only after a build has published it, so every view has an explicit state to show before that. And because the rows are files in the Workspace, everyone who can read the Workspace sees the same rows, and a query over them is the same query wherever you write it.

Each published port is a set of row-aligned files under the Workspace’s output folder, so line i of every companion describes row i of the rows.

File Holds
<port>.jsonl The rows, one JSON object per line
<port>.meta.json The port’s columns, its row count, and the table of data-style marks
<port>.lineage.jsonl Where each cell’s value came from
<port>.audit.jsonl The audit items the run recorded on the port
<port>.styles.jsonl The data-style marks on each row and cell, by mark id

A companion with nothing to say for any row is not written, so a port with no styles has no styles file and a reader treats it as unstyled. A Save File export keeps its styles in one sidecar object beside the exported file, stamped with that file’s version, and Explore paints it when you open the file in the Workspace.

Every grid and tile that reads published data answers the same question in the same words. When it has rows already, the same message appears as a mark beside them rather than in place of them.

Before a build has published anything

Message Detail
No run has been published yet. Run the workspace; the engine then publishes every node’s rows to the Workspace.
This port was not published by the last run. Run the workspace again with data reaching this node.
This port published no columns. The last run produced nothing to show here. Configure the node so it outputs columns, then run the workspace again.
Rows were not published. This port was over the size cap, or the run’s publish budget was already spent. The Workspace holds the port’s columns; its lineage and audit may have been left out too.
Run the workspace to read this workbook. Explore reads csv, tsv, json and jsonl directly; a run publishes each sheet of a workbook.

While the build loop has work for this workflow

Message Detail
A build is in progress A centred spinner with Rows publish when the run finishes. Counts, pills and ring colours on the canvas stay live.
A run is queued. The engine starts it shortly; these rows are from the last published run.
Changes are waiting for a run. Auto-build is off for this workspace, so these changes publish on the next run.

After the last build finished

Message Detail
The last run failed. These rows are from the last run that published.
Showing the last published run. The latest run finished without publishing these rows.

A previous run

Message Detail
This run kept no data. Only the newest runs keep their rows. Open a newer run, or the latest published run.

A view shows one of these at a time, and a build in progress wins: while a build runs you see the spinner, even when a second request is already queued behind it. The Run control is what tells you about that request, since it reads Queued next. After a build, what the last finished build says outranks the older “showing the last published run” message.

How many previous runs keep their rows is a Workspace setting: runs.keep in the Workspace’s .fontanaconfig.yaml, 20 by default (Namespace configuration). A run older than that, and any run from before per-run history existed, says This run kept no data.

A node’s Explore tab is editable for the node’s own output. Click a cell to select it, then press Enter or double-click to edit it. Headers are editable the same way.

An edit is not a local overlay. It is written to the Workspace as that port’s own edits file and committed in the Workspace’s history, so it survives a reload, everyone who can read the Workspace sees it, and you can find it in History on that file like any other change.

From the moment it is saved, the edit is part of what reads the rows:

  • Explore marks an edited cell with a yellow dot.
  • Filter, sort, search, group, the chart, the record count and the CSV and JSON downloads all read the edited value. Fontana Files applies the edits while it reads the rows, so nothing is overlaid afterwards on part of a page.
  • A query block over that port reads the edited value too. See Edited values and published values.
  • The edit marks the workflow as changed, so the next build takes the edited value as that node’s input and carries it through everything downstream.

An edit can stop applying, and Fontana Files says which one and why rather than dropping it silently:

Why What you see on the cell
The underlying value changed after you made the edit This cell changed since the edit was made, so the edit is not applied.
The column is no longer in the rows This column is no longer in the rows, so the edit is not applied.
The row is no longer in the rows This row is no longer in the rows, so the edit is not applied.
The value is an object or a list A nested column cannot be edited, so the edit is not applied.

A cell in that state shows its published value, and the next build skips that one edit while applying the rest.

Renaming a column header is a display label. SQL in a query block uses the file’s own column ids, so a rename never breaks a query you saved earlier.

A Markdown tile, a Stat tile and a node description all read published data the same way: a query block.

{% query late %}
SELECT region, COUNT(*)::INTEGER AS n
FROM ref('Clean orders')
WHERE "Order status" = 'late'
GROUP BY 1 ORDER BY n DESC
{% endquery %}
{{ late.table }}
{% for r in late.rows %}- {{ r.region }}: {{ r.n }}
{% endfor %}

The header is {% query <name> [sql|jsonata|jsonpath] [<source>] %}, and {% endquery %} closes it.

  • The block itself disappears from the rendered output. Its result is bound to <name> for the rest of the template, so you can use it in a sentence, a loop, a condition, or a Stat’s value.
  • The type defaults to sql. Write jsonata or jsonpath when you are reading the run record or a port’s columns.
  • The body is the query text exactly as you type it. It is never passed through the template language first, so a spaced column name needs no escaping and a quoted header is never double-escaped.
  • A block inside a fenced code block stays inert, so you can show one in a document without running it.
  • A name is bound once. Give each block its own name.

There is no inline form, and a dashboard has no Query tile: a Markdown tile with a query block in it is the way to put a query on a board.

A query names what it reads with one of five functions. They are functions rather than port names, so a port of your own called meta can never collide with them.

Function Reads Use in
ref('<node>') That node’s published rows, from its first output port sql
ref('<node>', '<port>') A named output port of that node sql
ref('<node>', '<port>', '<sheet>') One sheet of a workbook port sql
audit('<node>'[, '<port>']) That port’s audit items sql
lineage('<node>'[, '<port>']) That port’s lineage items sql
meta('<node>'[, '<port>']) That port’s published columns jsonata, jsonpath
run() The run record: status, timings, and each port’s row count and audit counts jsonata, jsonpath

Name a node by its title, or by its node id. A title that matches no node, or more than one, is a template error at that block, and the rest of the document still renders, so give the nodes you query distinct titles.

Inside a node description, ref() with no arguments means this node’s own output, and ref('<inputPortId>') means what this node read on that input port.

Two cases are deliberately not errors. A port that published no rows, and an audit() or lineage() call for a port with no items, both resolve to an empty result with no request sent at all, so {{ late.rowCount }} is 0 and a loop over late.rows renders nothing.

Field Holds
rows The result rows, as objects keyed by column
columns The result’s column names
rowCount How many rows came back
truncated true when the row cap was hit and more rows matched
value The first column of the first row; null when there are no rows
table The result as a Markdown table, with a showing N of N+ line when it is truncated
error The message when the block could not run; null when it ran

Whole numbers come back as numbers, so {% if total.value > 10 %} compares numbers rather than text. A number too large to hold exactly stays text rather than losing digits. Fontana Files’ own row column is dropped from columns, rows and table, so SELECT * shows your columns and nothing else.

A Stat whose value is null shows a dash, not an error.

A query block reads edited values where Explore does, and the published values everywhere else. You never switch this on; it follows what you are reading.

What you read What you get
ref() on a port whose rows someone edited The edited values, live, matching Explore
ref() on a port downstream of an edit The published values, which carry the edits as of the last build
audit(), lineage(), meta(), run() The published values; these are never edited
A workbook sheet The published values
A previous run (?runId=) That run’s published output, with no edits applied; an edit belongs to the rows it was made on, not to an older run

So a SUM in a Markdown tile over an edited column agrees with the total Explore shows for the same column, and the tiles downstream of it catch up on the next build.

A Stat can also show how many cells were edited: pick a node’s Edited metric in the Stat value picker. That count is computed when a build starts, so like every downstream reader it moves on the next build rather than the moment you type.

Limit Value
Query blocks per template Markdown tile 8, Stat tile 5 across its five fields, node description 2
Rows a block binds 200
Result size 1 MB per query
Time 10 seconds per query
Queries in flight 4 per page

A page with more tiles than that runs their queries a few at a time, and a dashboard only queries tiles that are in view. Identical blocks across tiles, Stats and node cards are answered once.

A block that cannot run splices **[Template error: …]** where it stood, carrying the message: a SQL error, a timeout, a result over the size cap, a port that was too large to publish, or an ambiguous node title. The rest of the template still renders, and {{ late.rows }} is empty. A Stat shows its single “could not be rendered” face, with the message in its tooltip.

While a build is in progress, a template host shows the spinner with Rows publish when the run finishes rather than a half-updated figure.

The first query after a build can wait while Fontana Files prepares the new rows for querying. The host says Preparing data… while it does, rather than timing out.

A view of a previous run reads that run’s own published output. When that run’s data was not kept, a query block cannot run and the host says Historical queries need per-run history.

The template editor completes {% query %} and the source functions, and offers the node titles and ports the last build published. It never offers a title that matches more than one node, or a port whose rows were not published, because neither can be queried.

An agent reads the same rows with the Fontana Files query tool, mcp_ffs_query, against the published files in the Workspace, with the edits applied the same way, so an agent dry-running a query sees the values the tile will show. See Tools and MCP.