The file format

An .omx file is plain text: constants, functions and sheets, in any order. A line that starts with # is a comment.

# Optional: the time zone of the file.
zone Europe/London

# A named value, and a function.
const TaxRate = 20%
fn WithTax(x) = x * (1 + TaxRate)

sheet Sales

# Optional: the type of a column.
Revenue : Decimal

# A cell that starts with = is a formula.
| Month | Revenue | Cost         | Profit | Gross |
| ----- | ------- | ------------ | ------ | ----- |
| Jan   | 10000   | 6000         | *      | *     |
| Feb   | 12000   | = 7000 + 500 | *      | *     |

# A computed column: one formula, every row. The header row names it where
# it goes, and its cells are *.
Profit := Revenue - Cost
Gross  := WithTax(Revenue)

Sheets

  • sheet Name, then a header row and data rows, written as a Markdown table: each line between outer pipes, | a | b |, cells separated by |, and a | --- | --- | line under the header row. A sheet is then a table wherever the file is shown as Markdown, a sheet of one column too.
  • The outer pipes and the --- line may be left out, as in older files: a | b. A header row that starts with | says the sheet has outer pipes. Without them, a | at the start or end of a row is an empty cell.
  • The app writes every sheet it changes as a Markdown table, lined up.
  • Name := expression after the rows is the formula of a computed column. The header row names the column, anywhere among the others, and each of its cells is *.
  • A computed column may be written as a sequence, Odd := 1, 3 ... Inf, which fills it from the terms given.
  • An empty cell is empty: there is no null.
sheet Sales

| Month | Revenue | Profit | Cost | Tax |
| ----- | ------- | ------ | ---- | --- |
| Jan   | 10000   | *      | 6000 | *   |
| Feb   | 12000   | *      | 7000 | *   |

Profit := Revenue - Cost
Tax    := Profit * 20%

Profit is the third column and Tax the fifth. A formula with no column of its name in the header row is an error, and so is anything but * in a cell of a computed column.

Types

Int, Decimal, Fraction, Percent, Num, Complex, Text, Bool, Date, Time, DateTime. A column with no Name : Type line takes its type from its cells.

Type Written
Int 42, 1_000_000
Decimal 19.99 — exact
Fraction 1/7, 22/7, 2 3/4 — exact, shown as a fraction
Percent 20%, 12.5% — exact, shown as a percentage
Num 1.5e3, Inf, -Inf, NaN — floating point
Text hi or "hi" (escape |)
Bool true, false
Date 2025-01-31
Time 09:30, 09:30:15
DateTime 2025-01-31T09:30

Text written bare may hold Markdown: a **slow** start, see the [lease](lease.pdf). The app shows it as that, and so does a rendered document. Text in quotes is as it is: "2 * 3 * 4". So is the text a formula makes, as it is written in quotes there; a value read from a cell is Markdown or not as that cell is. omasheet print shows every cell as it is written.

A Decimal, a Fraction and a Percent hold the same exact numbers, and a column of one takes the others, showing them its own way: a Decimal column shows 1/7 as 0.14285714285714285714…, and a Fraction column 0.25 as 1/4.

A Percent column takes any exact number and shows it as a percentage: 0.4 there is 40%, and so is = Profit / Revenue when it comes to two fifths. A 20% in a Decimal or Fraction column is the number it stands for, 0.2 or 1/5.

A Fraction column shows every number as a fraction, and one of more than one as a whole number and a part: 2.75 and 11/4 there are both 2 3/4. A fraction may be written that way too, with a space: 2 3/4 is 11/4, and the app keeps it in the file as 11/4.

See examples/types.omx for every type at work.

Dates and times are always written this way in the file. The app shows and accepts them in your locale’s form.

Constants, functions, zone

  • const Name = expression
  • fn Name(a, b) = expression — sees its parameters, constants and sheets, not the row it is called from. The comment above it is its description.
  • zone Area/City before the first sheet says which time zone the file’s date-times are in. Without it, they are in the zone of the machine.

Copyright (c) 2026 Stephen Roe. MIT licence.

Expressions (OMX)

Try any of these with omasheet eval [file.omx] '<expression>'.

Numbers

Arithmetic is exact: 1/3 + 1/6 is 0.5, 0.1 + 0.2 is 0.3, and integers do not overflow. x.Num gives a floating-point Num.

No exact number is a division by zero, so that gives a Num: 1/0 is Inf, -1/0 is -Inf and 0/0 is NaN. So does a function outside its domain: sqrt(-1) is NaN and ln(0) is -Inf. They are written as they are shown, Inf, -Inf and NaN, in a formula or a cell; the text is in quotes, "Inf". A NaN is not equal to anything, itself included: of the comparisons only != is true of one.

A number is shown in at most 23 characters, and a - before it. From 1e-15 up to 1e15 it is plain, 0.015625; smaller or larger it is in e notation, 1e-43 and 1e+60, and so is one too wide to be plain. An exact number in e notation keeps every digit it can, and where they do not all fit they are cut and marked: 2 ** 64 is shown as 1.8446744073709551…e+19. Only the display is cut, and omasheet eval --exact gives it in full.

A Decimal that does not end is cut and marked the same way: 1/3 is shown as 0.33333333333333333333…. Pointing at it in the app shows more places, and --exact gives it as a fraction, 1/3.

A Num is never rounded: it is shown with the fewest digits that give the same Num back, 0.30000000000000004, and always fits.

A Percent is an exact number shown as so many in a hundred: 20% is 1/5. It is rounded to two digits after the point, 41.67%, and --exact gives it in full, as a fraction where it does not end, 125/3%.

A Fraction is shown as a fraction where it fits: 1/3, and one of more than one as a whole number and a part, 11/4 as 2 3/4. One too wide is shown in e notation, cut and marked. --exact and x.Text give it as it is written in the file, 11/4.

Whole numbers and decimals divided give a Decimal, whether or not it ends: 7 / 4 is 1.75 and 7 / 3 is 2.33333333333333333333…, held exactly. Anything worked out from a Fraction is a Fraction: x.Fraction * 2. To show a result as a fraction, declare its column Fraction or write x.Fraction; for a decimal, write x.Decimal.

Numbers are shown the way Raku shows them:

Type Shown
Int 101; 1e+15 and 1.8446744073709551…e+19 from 1e15
Decimal 1.01; 1e-43 below 1e-15
Fraction 1/3, 2 3/4; 1.6129030457865418…e-01 when too wide
Percent 20%, 12.5%, 41.67%
Num 100, 1.5, 1.4142135623730951; 1e+21 or 1.5e-16 when very large or small; Inf, -Inf, NaN
Complex each part as a Num: 3+4i, 1.5-2i, 0+1.4142135623730951i; both parts cut alike when too wide

A Num that is a whole number looks like an Int, but it is still a Num.

A Decimal, a Fraction and a Percent are the same exact numbers shown in different ways: 19.99, 2 3/4 and 20%. Adding, subtracting, multiplying and dividing decimals gives a Decimal, and so does an average. Each goes anywhere another does: a Decimal column shows 1/3 as 0.333…. A column of no declared type with a decimal in it that does not end is a Fraction column, and shows every number in it as a fraction.

Percentages add up to a percentage: 20% + 5% is 25%, and so are their sum, average, least and greatest. Anything else done with one gives the plain number: 100 * 20% is 20, 1 + 20% is 1.2, and 20% == 0.2 is true. To show a result as a percentage, declare its column Percent or write x.Percent.

+ - * / **, == != < > <= >=, and or not, in, and a // b (b when a is empty). if c then a else b.

Converting

The name of a type converts to it, written x.Int, x.Int() or Int(x). A vector is converted value by value.

Written Gives
x.Int a whole number: 19.99 gives 19, -19.99 gives -19
x.Decimal the same number as a decimal: (1/8).Decimal is 0.125, (1/3).Decimal is 0.333…
x.Fraction the same number as a fraction: 2.75 gives 2 3/4; a Num as the decimal it is shown as
x.Percent the same number as a percentage: 0.4 gives 40%, 5/12 gives 41.67%
x.Num a floating-point number
x.Complex a complex number from a real one
x.Text any value as text, written the way a file writes it
x.Bool false for zero, true for any other number
x.Date, x.Time that half of a DateTime
x.DateTime a Date at midnight

Text is read the way a cell is: "42".Int is 42, "20%".Fraction is 1/5, "2025-01-31".Date is that date, and "abc".Int is an error. true.Int is 1. To round rather than drop the fraction, use round(x); round(x, 0.01) rounds to the nearest 0.01, and round(x, 100) to the nearest hundred.

Names

Written Means
Sales the sheet
Sales.Revenue the column, as a vector
Revenue inside the sheet: this row’s value
TaxRate a constant

Selecting with [columns; rows]

Written Means
Sales[Revenue; 1] one cell; positions count from 0
Sales[Revenue; -1] the last row
Sales[; 0..2] rows 0 to 2, every column; 0..^2 leaves out the end
Sales[; 0^..2] rows 1 to 2: ^.. leaves out the start, ^..^ both ends
Sales[; ^2] the first two rows: ^n is 0..^n
Sales[Revenue] the whole column: a missing or empty slot is all of it
Sales[; Revenue > 11000] the rows where it is true
Sales[Revenue; *-1] the row before this one; * is this row
Sales[Revenue; 0..*] from the top down to this row

Inside its own sheet the name can be dropped: [Revenue; *-1]. A row outside the sheet is empty, so [Revenue; *-1] // 0 starts a running value.

Sequences with ...

A computed column may be written as a sequence, terms ... limit. The terms given start the column and the rest follow a rule.

Written Gives
1, 3 ... Inf 1, 3, 5, 7, … — a constant difference
1, 2, 4 ... Inf 1, 2, 4, 8, … — a constant ratio
5 ... Inf 5, 6, 7, … — one term alone steps by one
0, 1, $-1 + $-2 ... Inf 0, 1, 1, 2, 3, 5, … — a rule of its own
1, 3 ... 9 1, 3, 5, 7, 9, then empty
"A", "B" ... Inf A, B, … Z, AA, AB, …
"Monday", "Tuesday" ... Inf the days of the week, round and round
"Jan", "Feb" ... Inf the months of the year
"Jan-2026", "Apr-2026" ... Inf quarters, carrying into the year
"AY" ... "BA" AY, AZ, BA, then empty
2025-01-01, 2025-01-08 ... Inf every seventh day
2025-01-01, 2025-02-01 ... Inf the first of each month
2025-01-31, 2025-02-28 ... Inf the last day of each month
  • $-1 is the term before this one, $-2 the one before that. The last term before ... is the rule when it reads one, and another starting term when it does not.
  • With no rule the step is taken from the terms: their difference, or their ratio where three or more share one. Two terms are read as a difference, so a constant ratio needs three.
  • Inf fills the column. Any other limit stops the sequence there and leaves the rows after it empty; a sequence that would step over its limit stops before it.
  • The days of the week and the months of the year are known by name, written in full or in three letters, in any case: they go round, so Sunday is followed by Monday and December by January. A month written with its year, Jan-2026 or January 2026, carries into the year instead. Every term must be written the same way, so "Mon", "Tuesday" is not one of these.
  • Any other text counts up the way letters and digits do, carrying at Z and 9, and only its last run of letters and digits moves: job-A9 to job-B0. succ(x) is that step on its own, and on a number it adds one.
  • Dates a fixed number of days apart step by days, and times and date-times by seconds. Dates on the same day of each month step by months instead, however many days that is, and the day they are counted from is the largest one given: 2025-01-31, 2025-02-28 ... Inf goes on to the 31st of March, not back to its 28th. One date alone steps by a day.
  • A sequence fills a column: it cannot change how many rows the sheet has, and ... Inf has no meaning anywhere else.

Durations

Next to a date, a plain number is days, and n.day, n.week, n.month and n.year say which: Placed + 1.month, Due - 2.week. Only a plain number takes a unit, so Placed.month is still the month of a date.

Months and years keep the day of the month, or the last day of a month too short for it: 2025-01-31 + 1.month is 2025-02-28. That rounding does not come back, so a sequence of month ends is better written with eom, which gives the last day of a date’s month: eom(2025-01-10), eom($-1 + 1.month) ... Inf.

Functions

Called as sum(x), x.sum() or x |> sum().

  • Vectors: sum avg min max count, of one vector or several taken together, with single values among them: sum([A; 0..3], [A; 7..9], 5). A block of cells counts every cell: sum([0..2; 0..3]).
  • Sheets: filter(condition), select(columns)
  • Maths: abs round floor ceil sqrt exp ln log10 log2 sin cos tan pi() e() and more
  • Counting up: succ — a number plus one, or the next text, AZ to BA
  • Converting: Int Decimal Fraction Percent Num Complex Text Bool Date Time DateTime
  • Dates: today() now() year month day weekday hour minute second date time
  • Months: add_months(x, n) — what x + n.month is — and eom(x)
  • Time zones: to_zone("Asia/Tokyo") utc() local() offset() zone()
  • Your own: fn in the file

The app lists them all under fn() (F1).

Dates and times

  • A whole number is days next to a Date, seconds next to a Time or DateTime: 2025-01-31 + 1 is 2025-02-01.
  • Two of a kind subtract to a number: 2025-03-01 - 2024-03-01 is 365.
  • Date + Time is a DateTime.
  • Across time zones, comparison and subtraction use the instant.

Copyright (c) 2026 Stephen Roe. MIT licence.

The app and the command line

The app

omasheet                # a new file
omasheet file.omx       # open a file, a workbook (.xlsx) or a CSV file

omasheet with no command opens the app. Needs Qt 6 (qt6-base, qt6-declarative). The text is the file: every edit rewrites the text, and saving writes exactly that.

  • One tab per sheet, plus the constants.
  • Two sheets can be shown side by side, each in a window of its own.
  • The box at the top shows the source of the current cell; the grid shows the value.
  • Under its name a column shows its type: muted if it comes from the cells, plain if it is declared, and after := in a formula column. Right-click a column’s header to Set type…, which starts on the type the column has now, or Clear type; a cell that does not fit a declared type is an error.
  • F3 shows the source of the whole file, to read or to edit. An edit is taken when the source is left, saved or closed, and undone as one step.
  • Selecting several cells shows their count, sum and average in the footer.
  • Beside that box is the selection, written as a formula would write it:

    Selected Shown
    One cell Sales[Revenue; 3]
    Cells of one column Sales[Revenue; 3..5]
    Cells of one row Sales[2..3; 3]
    A block of cells Sales[2..3; 3..5]
    A whole row, or rows Sales[; 3], Sales[; 3..5]
    A whole column, or columns Sales[Revenue], Sales[2..3]
    The whole sheet Sales

    Several columns are given by position, counting from 0.

  • Cells that are copied stay highlighted through as many pastes and inserts as you like, until Esc or the next copy. Cells that are cut stay where they are, highlighted, until they are pasted.
  • A number is shown in at most 23 characters: plain from 1e-15 to 1e15, 19.99 or 1/3, and in e notation otherwise, 1e+60. One with more digits than fit is cut and marked, 1.8446744073709551…e+19, and a Percent is rounded, 41.67%; pointing at it shows the value in full, as does pointing at any text cut off at the cell’s edge. A plain number of five digits or more is grouped in threes, and numbers follow your locale: 12,345.678 in Britain, 12.345,678 in Germany. The file does not: a number is typed as a file writes it, 12345.678, or 12_345.678 to group it, and copying a cell copies it that way.
  • Dates and times follow your locale; the file stays ISO.
  • A file in another time zone is shown on your clocks.
Keys  
Ctrl+. Context menu
Ctrl+? Keyboard shortcuts
Ctrl++ / Ctrl+- Zoom in / out (Ctrl+0 resets)
Ctrl+Z / Ctrl+Y Undo / redo
Ctrl+Arrows Go to end
Shift+Arrows Select block
Ctrl+Alt+Left/Right Previous / next sheet
Ctrl+C / X / V Copy / Cut / Paste
Ctrl+Shift+V Paste values: not formulas
Ctrl+Alt+V Paste transposed: rows as columns
Ctrl+Enter Insert row below (Shift: above)
Delete Clear
Ctrl+Delete Delete the selected rows, columns or sheet
Ctrl+O / S Open / save
Ctrl+Shift+S Save as
Ctrl+E Export as a workbook, CSV or JSON
Ctrl+K New constant
Ctrl+N New sheet after this one
Ctrl+Shift+N New window on this file
F1 View functions
F2 Edit
F3 View source
F11 Fullscreen

Two sheets side by side

  1. Open the file and go to the first sheet.
  2. Right-click the tab of the second sheet and choose Open in new window. Or press Ctrl+Shift+N for a second window on the same sheet, and pick the other tab there.
  3. On Omarchy the new window tiles beside the first. Elsewhere, place the two windows next to each other yourself.

Both windows show the same file, so an edit in one shows in the other at once, and undo and save work from either. Each window has its own sheet, cursor and text size. Cells cut or copied in one can be pasted in the other.

Closing one window leaves the file open in the other; the last one asks about unsaved changes. Opening another file, or a new file, changes every window.

Starting the app twice on the same file is not the same thing: those are two separate copies, and the one saved last overwrites the other.

The command line

omasheet print file.omx               # print the calculated file
omasheet eval '1/3 + 1/6'             # one expression
omasheet eval file.omx 'Sales.Revenue.sum()'
omasheet lint file.omx                # check without calculating
Option  
--locale show dates and times as this machine’s locale writes them
--now 2025-01-31T09:30 fix what today() and now() give
--zone Europe/London stand in for this machine’s time zone
--exact show every number in full: 18446744073709551616, not 1.8446744073709551…e+19, and 125/3%, not 41.67%

From the source tree, omasheet is cargo run --.


Copyright (c) 2026 Stephen Roe. MIT licence.

Workbooks, CSV, JSON and Markdown

The .omx file is the original. A workbook, a CSV file or JSON is a way in or out, and each command says what it could not carry across.

Import and export are a helper, not a full conversion. A file and a workbook are built differently: named, typed sheets and exact numbers on one side, a grid of cells and floating point on the other. Those differences keep either direction from covering everything, so check what the command reports.

Import

omasheet import budget.xlsx -o budget.omx    # a sheet for each worksheet
omasheet import sales.csv                    # one sheet, named after the file
omasheet import orders.json                  # a sheet for each list of rows
  • The first row is the column names. A name that is not an identifier is made one: Unit Price becomes UnitPrice.
  • A number is read exactly: 19.99 is 1999/100. A workbook holds every number as floating point, so it is read from the shortest decimal that gives the same number; CSV is read as it is written, every digit of it.
  • A column of dates, times, date-times or true / false is given its type. A column of numbers and text together has none: each cell is what it reads as.
  • A formula in a workbook is translated to OMX where OMX can say it: see Formulas. Where it cannot, the value is imported, and the cells are listed with the reason.
  • Text that looks like a formula, =SUM(A1:A3), stays text.
  • Text is imported as it is, not as Markdown: *new* is written in quotes, "*new*", so its asterisks are kept.

JSON

JSON is read as values: a list of rows is a sheet named after the file, and an object of such lists is a sheet for each, named by its key.

{
  "Sales": [
    {"Month": "Jan", "Revenue": 10000, "Day": "2025-01-31"},
    {"Month": "Feb", "Revenue": 12000, "Day": "2025-02-28"}
  ]
}
  • A row is an object of values by column name. The columns are in the order they are first met, and one that a row does not have is empty there. A row may be a list of values instead, with the names as the first row.
  • A number is read as it is written, every digit of it: 19.99 is a Decimal, and 1.5e3 a Num. true and false are Bool, and null is an empty cell.
  • JSON has no dates, so a string that is a date, a time or a date-time is one. So is a string that is a Complex, "3+4i", or "Inf", "-Inf" or "NaN". Any other string is text, a number in quotes too: "42".
  • A list or an object inside a row is kept as text, and reported.

Formulas

A worksheet becomes a sheet whose first row is the header, so a cell of a formula becomes a column and a row:

In the workbook, on row 3 In the file
=B3-C3 = Revenue - Cost
=B2 = [Revenue; *-1], the row above
=B$2 = [Revenue; 0], the first row whichever row asks
=SUM(B2:B10), all the rows = Sales.Revenue.sum()
=SUM(B$2:B3) = [Revenue; 0..*].sum(), a running total
=Rates!B2*B3 = Rates[Rate; 0] * Revenue
=IF(B3>C3,"up","down") = if Revenue > Cost then "up" else "down"
  • A column filled down with one formula becomes a computed column, with * in its cells and Profit := Revenue - Cost after the rows.
  • These are translated: + - * / ^ % and the comparisons; SUM, AVERAGE, MIN, MAX, COUNT, COUNTA; IF, IFS, SWITCH, AND, OR, NOT; ROUND, INT, TRUNC, ABS, SIGN, SQRT, POWER, EXP, LN, LOG10, PI and the trigonometry; TODAY, NOW, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY, DATEVALUE.
  • These are not, and the value is imported: joining text with &, lookups such as VLOOKUP, DATE(y, m, d), names defined in the workbook, lists in { }, and a cell in the header row or outside the sheet.
  • Every translation is checked. The file is worked out, and a formula that does not give the value the workbook holds for its cell is dropped for that value. The commonest cause is an empty cell: a workbook adds it up as nothing, and in OMX what is worked out from an empty cell is empty. TODAY() and NOW() are not checked, as they give another answer each day.
  • A workbook counts the days of the week from Sunday and OMX from Monday, so WEEKDAY(A2)=1 becomes weekday(Date) == 7.
  • A SWITCH or an IFS with nothing for when no case fits takes its last case for that.

Export

omasheet export budget.omx --xlsx            # budget.xlsx, a worksheet for each sheet
omasheet export budget.omx --csv             # budget.Sales.csv, budget.Summary.csv
omasheet export budget.omx --csv --sheet Sales -o sales.csv
omasheet export budget.omx --json            # budget.json, every sheet
omasheet export budget.omx --json --sheet Sales   # the rows of Sales alone

A workbook is written with the formulas of the file, and each with its calculated value, so it reads the same before anything works it out again. CSV and JSON hold the calculated values alone. Constants are not exported: a formula that reads one is written with what the constant is.

A sheet becomes a worksheet whose first row is the header, so a column and a row become a cell. Each formula is written for the row it is on:

In the file, on row 1 of Sales In the workbook
Revenue - Cost =B3-C3
[Revenue; *-1], the row above =B2
[Revenue; 0], the first row =$B$2
Sales.Revenue.sum(), from another sheet =SUM('Sales'!$B$2:$B$13)
sum([Revenue; 0..*]), a running total =SUM($B$2:B3)
[Revenue; *-1] // 0 =IF(ISBLANK(B2),0,B2), and 0 on the first row
Rates[Fee; Code == Sales[Code; *]] // 1 =IFERROR(INDEX('Rates'!$B$2:$B$9,MATCH(A3,'Rates'!$A$2:$A$9,0)),1)
count(Orders[; Region == Name]) =COUNTIFS('Orders'!$F$2:$F$40,A3)
if Revenue > Cost then "up" else "down" =IF(B3>C3,"up","down")
  • These are written: + - * / ** and the comparisons; and, or, not, if; sum, avg, min, max and count of a column, a range of rows or a list; sum, avg and count of the rows where columns equal values; a row found where one column equals a value; abs, round, floor, ceil, sqrt, the logarithms and the trigonometry; today, now, year to second, weekday, date and time; x.Int, and a function the file defines.
  • These are not, and the value is written: a calculation on a whole column at once, Sales.Revenue * 2; rows picked by any other condition, by a list or by a range value; in and ranges as values; // after a calculation; text conversions; time zones; complex numbers. Each is reported with the columns it happened in.
  • A cell that comes to nothing, or that failed, is left empty, with no formula.
  • A workbook counts a time in days and a file in seconds, so At - 09:00 is written =ROUND((C3-TIME(9,0,0))*86400,0).
  • A sheet exported alone with --sheet cannot read another: those formulas are written as values.
  • A formula is not checked in a workbook, as nothing here works one out. It is exact in the file and floating point there, so the last digits may differ.
  • Importing the workbook again brings back most formulas, but not all: the import has no translation for ISBLANK or IFERROR, and takes those cells as values.
  XLSX CSV
Int, Decimal, Fraction the nearest floating-point number in full; one that does not end, such as 1/3, as 0.3333333333333333
Percent that number, formatted as a percentage 40%; one that does not end as 33.33333333333333%
Date, Time, DateTime a date or time cell 2025-01-31, 09:30, 2025-01-31T09:30
Complex text 3+4i
a cell that failed empty empty

JSON is written as it is read: an object holding each sheet by name as a list of rows, a row to a line, and each row an object of values by column name. With --sheet it is the rows of that sheet alone.

  JSON
Int, Decimal, Fraction a number, in full; one that does not end, such as 1/3, as 0.3333333333333333
Percent the number it stands for: 20% is 0.2
Num a number; Inf, -Inf and NaN as strings
Text, Bool a string; true or false
Date, Time, DateTime, Complex a string, as a file writes it: "2025-01-31", "3+4i"
an empty cell, or one that failed null

Anything written less exactly than the file holds it is reported, with the columns it happened in.

In the app

  • Ctrl+O opens a workbook, a CSV file or JSON as well as an .omx file. It is imported as a new file, not yet saved.
  • Ctrl+E exports, and asks what as: every sheet as a workbook (X) or as JSON (J), or the sheet shown as CSV (C). It then asks where.
  • What was changed, rounded or left behind is listed, and stays under notes in the footer.

Markdown

omasheet render report.md                    # Markdown, with the values in place
omasheet render report.md --html -o report.html

A Markdown document can quote a file:

---
files: [budget.omx]
---

Revenue was {{ Sales.Revenue.sum() }}, best in {{ Sales[; Profit == Sales.Profit.max()].Month }}.

```omx
sheet Targets

| Month | Goal | Over |
| ----- | ---- | ---- |
| Jan   | 4200 | *    |

Over := Sales[Profit; *] - Goal
```
  • files: in the front matter names .omx files, from where the document is. files: budget.omx and a list of - budget.omx lines work too.
  • A fenced omx block is a file, shown as its sheets. The files and all the blocks of a document are one file, so each can read the others.
  • {{ expression }} is replaced by its value. Several values read as a list, Feb, Mar; a sheet is shown as a Markdown table.
  • {{ in code is left alone, and so is \{{.
  • Text written bare in a cell may hold Markdown, and is read as that in the document: **bold**, [a link](there.md). Text in quotes, or made by a formula, is shown as it is. HTML in a cell is shown as text.
  • A mistake is reported at its line in the document, or in the file it is in, and nothing is written.

A tool that does not know Omasheet shows the block as code and the braces as they are. examples/report.md is a worked document, with what it renders as beside it.


Copyright (c) 2026 Stephen Roe. MIT licence.