The .012 format
.012 files are JSON with one line per cell, keyed by address, so diffs
read naturally and files merge reasonably in version control. This page
is the format at version 7, the newest; Versions says what
each version adds and what 012 promises about old files. A cell
without formatting is just what was typed; a formatted cell is a small
object:
{
"version": 2,
"widths": {"A": 14},
"cells": {
"A1": "Rent",
"B1": {"input":"1450","format":"currency","decimals":2},
"B2": {"input":"95","note":"Due on the 1st"},
"B3": "=SUM(B1:B2)"
}
}
A formatted cell has its input and the formatting that isn't the
default: format (as number_format in
macros names it: currency, percent, date, ...), decimals, pattern for a
custom format, bold, italic, underline, strikethrough,
align (left, center, right), valign (top, middle,
bottom), wrap (wrap or clip) and borders, each edge's line by
name, with topColor, bottomColor, leftColor and rightColor for
a line in a color:
"borders":{"top":"thin","bottom":"double","left":"thick","right":"thin","bottomColor":"red"}
(formatting).
Only what's typed is saved. What 012 computes is computed again when the file opens: formula results, pivot tables' results, and the arrays formulas spill, whose cells are kept only for their formatting and notes, and the rows of regions, made again by their commands or read again from their files.
Reading and writing
Any JSON with the same fields reads the same: the order of keys,
spacing, and the case of field names don't matter. A cell given twice
takes its last entry, though an invalid earlier one is refused; the
version or the sheets list given twice around cells is refused.
012 writes one form of each workbook: the fields in the order this page shows them, cells by row and then column, widths and heights by position, column and row formats with the whole sheet's first, then columns', then rows', names by name, and charts, rules, regions and macros in the order they were made. Saving a file 012 wrote, without changing it, gives back the same bytes.
012 reads and writes the file as a stream, each cell straight into or out of its sheet, so opening a file takes about the memory of the workbook however large the file; what that costs, and why there is no binary format beside this one, is in Bounds of support.
Versions
012 writes the lowest version that holds the workbook, so older builds open what they can, and refuses a version newer than it knows rather than lose sheets or pivots. Version 1 files (cells as plain strings) still load.
| Version | Written when | Adds |
|---|---|---|
| 2 | One sheet, using none of the below | cells, widths |
| 3 | A sheet has named ranges, frozen panes or a filter | names (each name's range, #REF! once its cells were deleted), freeze (rows, cols), filter (its range, and columns by letter, each with hidden values or a condition and its value) |
| 4 | Several sheets, or a formula naming a sheet | sheets, a list; see Several sheets |
| 5 | A pivot table | a sheet's pivot; see Pivot tables |
| 6 | A table | a sheet's tables; see Tables |
| 7 | A linked source | a sheet whose region is paged; see Linked sources |
What stays the same from release to release:
- Every file a release saved opens in every later build, and saving it
unchanged gives back its bytes. Each release keeps workbooks it saved
as fixtures (
internal/sheet/testdata/fixtures/<release>), which every build's tests open and save again (Releasing). - Fields are only added, never renamed or given another meaning; the version rises only for a field older builds can't do without, and the rest are listed under Fields that need no version.
- An older build ignores a field it doesn't know, but refuses a value it doesn't know in a field it does (a chart type, a rule's condition such as a date period), rather than open the file with the rule or chart quietly changed.
Fields that need no version
Builds that don't know these fields ignore them (and drop them if they save), so they raise no version:
| Field | On | Holds |
|---|---|---|
note | a cell | Its note |
own | a cell | true when its formatting is its own, not its column's or row's (Automatic in a currency column) |
text | a cell | How its input reads when its format would say otherwise: true for text typed into a Plain text cell whose format changed since ({"input":"007","text":true}), false for a number or formula typed before the cell was formatted as Plain text; either stays as typed, as in Sheets. Older builds read the input as the format says |
wrap, borders | a cell or a line | How its text wraps and its borders; older builds show the text overflowing, without lines |
valign, and colors in borders | a cell or a line | Where its text sits in a tall row, and its borders' colors; older builds show the text at the bottom and the lines in the text's ink |
lines | a sheet | Column and row formats |
heights | a sheet | Row heights set by hand, in lines, by row number: "heights": {"3": 2, "7": 4} |
merges | a sheet | Merged cells, by range: "merges": ["A1:C1", "D2:D5"] |
name, hidden | a sheet | Its name when renamed; true when hidden (a file whose sheets are all hidden opens with the first one shown) |
charts | a sheet | One chart per line: type (column, bar, line, pie, area, scatter), data, at, width, height, byRow, header, labels, title, and options left out at their defaults: stack (stacked, percent), trend, min, max, log, gridlines (only when off), legend (right, none). A build that charts but lacks a chart's type refuses the file |
protected | a sheet | Protected ranges: {"range":"B2:C9","description":"Totals"}, or {"sheet":true} |
conditionalFormats, validations | a sheet | Rules, one per line |
arithmetic | the workbook | decimal for decimal arithmetic |
locale | the workbook | The locale it's typed and shown in ("de-DE"), when File > Settings > Locale chose one; without it, the file follows the reader's locale setting. Cells are stored the same way in every locale: input is always as typed in en-US (1,234.5, 9/26/2026, =ROUND(A1,2)) |
macros, macroOrigin | the workbook | Macros as Starlark scripts, and the computer they were made or trusted on (macros', notebook cells' and linked files' alike): see Macro scripting API. Opening a file never runs them |
regions | a sheet | Its linked files and notebook cells' outputs sent there: see Regions. Older builds open the sheet without them |
tab, reactive, notebookCells | a sheet | A notebook tab and its cells: see Notebooks. Older builds open it as an empty sheet |
Column and row formats
Formats of whole columns and rows are kept
in a lines field after the widths, runs of lines with the same
formatting together; A:XFD is the whole sheet's format:
"lines": {"A:XFD": {"italic":true}, "B:D": {"format":"currency","decimals":2}, "1:1": {"bold":true}},
Several sheets
In version 4 the sheets are a list, each with its name and the fields a
version 3 file has at the top, and named ranges say their sheet. The
workbook's settings (named ranges, decimal arithmetic, the locale, the sheet shown
when saved as active) stay at the top:
{
"version": 4,
"names": {"Rent": "Budget!B1"},
"active": 1,
"sheets": [
{
"name": "Budget",
"cells": {
"A1": "Rent",
"B1": "1450"
}
},
{
"name": "Q3 plan",
"hidden": true,
"cells": {
"A1": "=Budget!B1*3"
}
}
]
}
Pivot tables
A pivot's sheet has a pivot field, on one line after its cells,
holding the definition. The results are never saved; they are computed
again when the file opens, so the file stays small and can't disagree
with its data.
{
"name": "Pivot Table 1",
"cells": {},
"pivot": {"source":"Sales!A1:D200","rows":[{"column":"B"},{"column":"A","order":"desc","sortBy":1}],"columns":[{"column":"C"}],"values":[{"column":"D","summarize":"sum"},{"column":"D","summarize":"counta","showAs":"percent_of_total","name":"Share"}],"filters":[{"column":"A","hidden":["North"]}],"rowTotals":true,"columnTotals":false}
}
sourceis the data, with its sheet;Sales!#REF!once the range was deleted. The sheet is by name, as in formulas, and follows renames.rowsandcolumnsare fields by column letter on the source sheet, withorderdescfor Z to A andsortBythe value, counting from 1, whose totals order the groups.valuessummarize a column:sum,counta,count,countunique,average,max,min, orrows(every row, blank or not, which frequency tables use);showAsispercent_of_row,percent_of_columnorpercent_of_total;namereplaces the header.filterstake a filter column's criteria:hiddenvalues and aconditionwith itsvalue.rowTotalsandcolumnTotalsare the grand total row and column.
Tables
A sheet's tables are a tables list among its
view's fields, before its cells:
"tables": [{"name":"Sales","range":"A1:C20","columns":["Region","Month","Amount"],"banded":true,"header":true}],
nameis what formulas name it by (Sales[Amount]).rangeis its cells, header row included.columnsare its columns' names, left to right, as its header row shows them. Left out, or of another number than the range's columns, they're read from the header row.bandedshades every other data row, andheaderstyles the header row.
Tables need version 6: a build without them can't read the formulas that read them, and refuses the file rather than show them as errors.
Regions
A sheet's regions, linked files and notebook cells'
outputs sent there, are a
regions list after its cells, charts and rules, one per line, in the
order they were made. The file keeps where each region is and what feeds
it, never its table: opening the file reads linked files again and sends
each output from its cell's saved output.
"regions": [
{"name":"big","at":"A1","output":true},
{"name":"app","at":"F1","path":"logs/app.csv","window":500}
]
nameis what formulas name it by (nu.big,nu.app) and notebook cells read a linked file as ($app); an output's is its cell's.atis its table's first cell, the header row's.outputmarks a notebook cell's output.- A linked file has
path: the file, relative to the workbook's folder when it's saved there or below, and otherwise as it was linked.formatnames the file's format (CSV,TSV,JSON,NUON,XLSX,SQLite,Parquet) when the extension doesn't tell it,tableorquerysay what to read from a SQLite database, andwindowkeeps the last rows under the header.
The table's cells are saved only for their formatting and notes, as a
spill's are. A region with a command, which the notebook sheets of
earlier versions wrote (with input, the range it read as $in, and
rows, cols, reads and sort, which opening it doesn't need),
opens as a code cell of the workbook's notebook, its table sent where it
was (Notebook sheets).
Linked sources
A linked source's tab is a sheet with no cells whose one
region, at A1, is paged: the file keeps what the source reads and
how its tab orders the rows, never the rows.
{
"name": "trips",
"cells": {},
"regions": [
{"name":"trips","at":"A1","path":"trips.parquet","paged":true,"order":{"sort":[{"column":"C","desc":true}],"filter":[{"column":"B","condition":"eq","value":"card"}]}}
]
}
nameis the table formulas read (trips[fare],nu.trips); the tab is named the same when linked, and keeps its own name when renamed.path,format,tableandqueryare what a linked file's are (Regions).pagedmarks the region as a source, read in place.orderis how the tab sorts and filters the rows:sort, the columns by letter,descfor Z to A, andfilter, each a column by letter with aconditionand itsvalue, as a filter's column has (Filter). Formulas read the source in its own order whatever the tab shows.
Sources need version 7: a build without them would read the region as a linked file and load the whole source into cells.
Notebooks
A notebook tab is a sheet with "tab": "notebook", "reactive": true
when it's reactive,
and its cells in notebookCells, one per line:
"tab": "notebook",
"notebookCells": [
{"kind":"note","source":"# Files"},
{"source":"files = ls","output":"[[name, size]; [go.mod, 812b]]"},
{"source":"$files | nope","error":"Command `nope` not found","detail":"help: ..."},
{"source":"ls **/*","unsaved":true}
]
kindisnotefor Markdown, left out for code;sourceis what the cell holds, a code cell's name and all, asfilesabove.outputis what the cell's last run printed, as NUON text, kept when it fits thenu-save-cell-kbandnu-save-notebook-kbcaps (Configuration);unsavedmarks one left out for its size.erroranddetailare a failed run's message and help line,notewhat an output left out.
Run counts, timings and stale marks aren't saved: an output read from the file is what its cell's source and inputs gave.
Conditional formats and data validation
A sheet's rules are two lists after its cells and charts, one rule per line:
"conditionalFormats": [
{"ranges":"A2:A6","condition":"formula","values":["=$D2"],"text":"green","strikethrough":true},
{"ranges":"C2:C6","scale":[{"type":"min","color":"green"},{"type":"percentile","value":"50","color":"yellow"},{"type":"max","color":"red"}]},
{"ranges":"E2:E6","dataBar":{"color":"blue","min":{"type":"min"},"max":{"type":"max"}}},
{"ranges":"F2:F6","iconSet":{"icons":"arrows","points":[{"type":"percent","value":"33"},{"type":"percent","value":"67"}]}}
],
"validations": [
{"ranges":"B2:B6","criteria":"list","items":["Ann","Bo","Cy"],"help":"Pick who does it"},
{"ranges":"D2:D6","criteria":"checkbox"},
{"ranges":"C2:C6","criteria":"number","condition":"between","values":["0","80"],"reject":true}
]
rangesare the rule's ranges,A2:A9,C2:C9.- A single-color rule has a
condition(empty,not_empty,contains,not_contains,starts_with,ends_with,exactly,date_is,date_before,date_after,gt,ge,lt,le,eq,ne,between,not_between,formula, and those comparing a cell with the rest:top,bottom,top_percent,bottom_percent,above_average,below_average,duplicate,unique), itsvaluesas typed (a date rule's may be a period:this week), and its style:textandfillcolors (red,yellow,green,cyan,blue,magenta) andbold,italic,underline,strikethrough. - A color scale has
scale: two or three points, each atype(min,max,num,percent,percentile), avaluewhere it takes one, and acolor. - A data bar has
dataBar: itscolor, itsminandmaxpoints as a scale's, andbarOnlyto hide the values. An icon set hasiconSet: itsicons(arrows,circles,symbols,rating), thepointswhere each icon after the first starts,reversefor the other way round andiconOnlyto hide the values. Older builds leave both out. - A validation rule has
criteria(list,range,checkbox,number,date,length,formula) with itsitems,source(Lists!A1:A20), orcondition(between,not_between,eq,ne,gt,ge,lt,le; none for any date) andvalues;rejectrefuses invalid entries, andhelpreplaces the rule's own help text. A checkbox'sitems, when it has them, are its checked and unchecked values (["Yes", "No"]); a dropdown'sdisplayischiporplainwhen it doesn't show the arrow. Older builds draw every dropdown with its arrow, and a checkbox with values of its own as invalid entries.
A line of either list is also what Add conditional format rule and Add
data validation rule take (in the palette, and as run(..., answer=...) in macros), which is how a macro records a rule added from
the panel.