Data in and out
Rows come into a notebook from the model and the project’s sources, as tables that never change. What the notebook finds goes back into the model as a version a person approves.
From the model: OAX steps
An OAX step runs a query on the model, with its measures, relationships and roles, and its rows become a Table named after the step. A column keeps a short name, Product for Products[Product], and is found by either. Numbers, text, dates and true/false keep their kinds; a blank is a missing value.
Prefer the model’s measures in these queries. The notebook then counts as the report pages count, and when a measure changes, the notebook follows.
SQL steps
A SQL step runs DuckDB SQL over the model’s tables, each by its name, and the project’s sources. Its rows become a variable the same way:
-- DuckDB over the model's tables (and the project's sources)
SELECT Product, SUM(Liters) AS Liters, COUNT(DISTINCT Shop) AS Shops
FROM Sales
GROUP BY Product
ORDER BY Liters DESC
| Product | Liters | Shops |
|---|---|---|
| Pils | 1,004,326 | 15 |
| IPA | 331,884 | 15 |
| Cider | 301,074 | 15 |
| Lettøl | 169,179 | 15 |
SQL reads the tables’ rows; OAX goes through the model, its measures, relationships and roles. Use OAX for anything a report would show.
Queries from C#
A C# step can query too: Dax(…) and Sql(…) return a table. Written with $"…", a date, a number or a list in braces goes in as a literal of the language, so a query never depends on how a date is written on this computer:
// Dax($"…") writes values in braces as OAX literals: a date, a number, a list
var from = new DateTime(2026, 9, 1);
var products = new[] { "IPA", "Pils" };
Dax($"EVALUATE CALCULATETABLE ( SUMMARIZECOLUMNS ( Sales[Month], Products[Product], \"Liters\", [Liters sold] ), Sales[Month] >= {from}, Products[Product] IN {products} )")
| Month | Product | Liters |
|---|---|---|
| 2026-09-01 | IPA | 9,541 |
| 2026-09-01 | Pils | 27,836 |
| 2026-10-01 | IPA | 8,823 |
| 2026-10-01 | Pils | 27,023 |
A list’s text values are quoted for you. Text on its own goes in as it is, as code such as a column’s name: to compare with a text value, write its quotes yourself.
Tables
A table never changes. Every method that filters, sorts, adds or joins gives a new table, so a step can never spoil what an earlier one made:
EVALUATE SELECTCOLUMNS ( Products, "Product", Products[Product], "Category", Products[Category], "Price", Products[Price per liter] )
| Product | Category | Price |
|---|---|---|
| Pils | Beer | 38 |
| IPA | Beer | 64 |
| Cider | Cider | 52 |
| Lettøl | Beer | 30 |
// join on the shared column, add a column, filter, sort: each a new table
bySql.Join(products, "Product")
.With("Revenue", r => r.Number("Liters") * r.Number("Price"))
.Where(r => r.Number("Liters") > 200_000)
.OrderBy("Revenue", descending: true)
| Product | Liters | Shops | Category | Price | Revenue |
|---|---|---|---|---|---|
| Pils | 1,004,326 | 15 | Beer | 38 | 38,164,388 |
| IPA | 331,884 | 15 | Beer | 64 | 21,240,576 |
| Cider | 301,074 | 15 | Cider | 52 | 15,655,848 |
t["Liters"]is a column,t[0]the first row;r.Text("Product"),r.Number("Liters")andr.Date("Month")read a row’s values.Where,OrderBy,Head,Tail,Sample,Select,Without,Rename,With,Join,LeftJoinandTable.Concatgive new tables.- LINQ works over
t.Rows, and.ToTable()makes the rows a table again;Table.FromRecordsmakes one from objects, such as aGroupBy’s.
Every member is in the Tables reference.
Dates to the millisecond
Dates and times reach a C# step to the millisecond:
EVALUATE ROW ( "At", DATE ( 2026, 11, 2 ) + TIME ( 8, 30, 15 ) + 0.25 / 86400 )
| At |
|---|
| 2026-11-02 08:30:15 |
// date-times reach C# to the millisecond
stamp[0].Date("At").ToString("yyyy-MM-dd HH:mm:ss.fff")
2026-11-02 08:30:15.250
The table under an OAX step shows times to the second; the value is whole.
Back into the model
A notebook’s results go back into the model as a version, never by writing to its tables directly. In an operations research, quantitative or network notebook, Results.Save keeps rows, a plan, a valuation or a network:
EVALUATE SELECTCOLUMNS ( Demand, "Product", Demand[Product], "Liters", Demand[Liters] )
| Product | Liters |
|---|---|
| Pils | 150,000 |
| IPA | 60,000 |
| Cider | 50,000 |
| Lettøl | 40,000 |
// next month's plan: 5 % over the demand. A draft version: approved and in use, its rows are a table of the model
var plan = demand.With("Planned", r => Math.Round(r.Number("Liters") * 1.05));
Results.Save("Demand plan", plan, key: "Product", note: "5 % over the demand")
Demand plan v1 (draft), saved 2026-11-06 19:08 by Kari Nordmann, fingerprint 3cc38fc2b714
The version is a draft. Once a person approves it and puts it in use, in the notebook’s Versions tab, Use in the model makes its rows a table of the model named after the notebook, related by its key: it refreshes, relates and is secured like any table. Another version put in use replaces its rows. In a notebook, Results.Load reads a version’s rows again:
// a version's rows again: the version given, else the one in use, else the newest
Results.Load("Demand plan")
| Product | Liters | Planned |
|---|---|---|
| Pils | 150,000 | 157,500 |
| IPA | 60,000 | 63,000 |
| Cider | 50,000 | 52,500 |
| Lettøl | 40,000 | 42,000 |
A prediction saves a model instead, with Models.Save; once a version is in use, PREDICT("name") scores with it in OAX (see Prediction). Versions and approval has the rest.