OpenBI Docs Buy

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
ProductLitersShops
Pils1,004,32615
IPA331,88415
Cider301,07415
Lettøl169,17915

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} )")
MonthProductLiters
2026-09-01IPA9,541
2026-09-01Pils27,836
2026-10-01IPA8,823
2026-10-01Pils27,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] )
ProductCategoryPrice
PilsBeer38
IPABeer64
CiderCider52
LettølBeer30
// 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)
ProductLitersShopsCategoryPriceRevenue
Pils1,004,32615Beer3838,164,388
IPA331,88415Beer6421,240,576
Cider301,07415Cider5215,655,848
  • t["Liters"] is a column, t[0] the first row; r.Text("Product"), r.Number("Liters") and r.Date("Month") read a row’s values.
  • Where, OrderBy, Head, Tail, Sample, Select, Without, Rename, With, Join, LeftJoin and Table.Concat give new tables.
  • LINQ works over t.Rows, and .ToTable() makes the rows a table again; Table.FromRecords makes one from objects, such as a GroupBy’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] )
ProductLiters
Pils150,000
IPA60,000
Cider50,000
Lettøl40,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")
ProductLitersPlanned
Pils150,000157,500
IPA60,00063,000
Cider50,00052,500
Lettøl40,00042,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.

Last updated · OpenBI Studio 0.3.78