Scripts
Python and R on the canvas — read your tables as data frames, compute anything, and emit new tables and tiles.
A script is a Python or R program that lives on the canvas: it reads the document's tables and tiles, computes whatever the language can compute, and emits new tables and tiles as canvas objects. Scripts never run automatically — you click Run.
A script's language is chosen when you create it and doesn't change afterwards. Both languages get the same editor, the same lineage arrows, the same refresh behaviour, and the same runtime API — spelled in each language's own idiom.
Requirements
Scripts use your own interpreter — nothing is bundled:
| Language | Needs | Found as |
|---|---|---|
| Python | pandas and duckdb | python, py, python3 |
| Python, for statistical tests | also scipy and statsmodels | as above |
| R | duckdb and jsonlite (with DBI) | Rscript |
A language with no usable interpreter keeps its entry points hidden — if you have Python but not R, you'll simply never see R. Point DreamSheets at a specific interpreter (a virtualenv, a non-PATH R) in the Scripts section of AI Settings. Each interpreter stays warm between runs, so repeated runs don't pay the import tax.
Writing a script
Right-click empty canvas and pick Add Python script here… or Add R script here… — only the languages you have installed are offered. Insert → Python Script… / R Script… does the same thing. Either opens the script editor: a syntax-highlighted source editor, a built-in API cheat-sheet, an optional Suggest with AI helper that drafts the script from a description, and a Run button.
The script environment, in both languages:
| Python | R | What it is |
|---|---|---|
sheet.MyTable | sheet$MyTable | The table named MyTable |
sheet["My Table"] | sheet[["My Table"]] | The same, for names with spaces |
.to_df() | (not needed) | Python's handle is lazy; R hands you the data frame directly |
sheet.sql("SELECT …") | sheet$sql("SELECT …") | Run DuckDB SQL across the document's tables |
tiles["My Tile"] | tiles[["My Tile"]] | A tile's value |
emit_table(df, name="…") | emit_table(df, name = "…") | Create/update a table on the canvas |
emit_tile(value, name="…") | emit_tile(value, name = "…") | Create/update a tile |
pd, duckdb pre-imported | base R attached |
orders = sheet.Orders.to_df()
by_month = (orders
.assign(month=pd.to_datetime(orders["Date"]).dt.to_period("M").astype(str))
.groupby("month", as_index=False)["Amount"].sum())
emit_table(by_month, name="MonthlyRevenue")
emit_tile(orders["Amount"].sum(), name="TotalRevenue")orders <- sheet$Orders
orders$month <- format(as.Date(orders$Date), "%Y-%m")
by_month <- aggregate(Amount ~ month, data = orders, FUN = sum)
emit_table(by_month, name = "MonthlyRevenue")
emit_tile(sum(orders$Amount), name = "TotalRevenue")If a script ends with a bare result value and emits nothing explicitly, DreamSheets emits it for you — a data frame becomes a table, anything else becomes a tile.
For large tables, prefer sheet.sql(...) / sheet$sql(...) — the query is pushed into DuckDB over the table's storage instead of materializing everything into memory first. This matters more in R, where sheet$MyTable loads the whole table.
Python extras
Python's table handle is lazy, so it also offers .batches(n=100_000) to iterate the table in Arrow record batches (out-of-core for huge tables) and .schema for the table's schema as a DataFrame. Python scripts also get IDE-style autocomplete in the editor; R's editor is plain for now.
R notes
R scripts auto-print visible top-level results, like the R console — a bare head(df) shows its output. Base R is attached; anything else (dplyr, tidyr, a modelling package) must be installed in the R that DreamSheets is using.
Refresh, lineage, and iteration
- Outputs are keyed by the script's own emit names: re-running the script updates the tables and tiles it created in place rather than creating duplicates. The match is on the script's recorded outputs, never on a document-wide name — so a script's first run that emits
name="Sales"into a document that already has aSalestable createsSales (2)rather than overwriting someone else's table. Script-produced cards get Refresh script / Edit script… in their menus, and a refresh always re-runs in the script's own language. - Column types follow the data frame, via DuckDB: integer dtypes land as integer columns, floats as numbers,
datetime.datevalues (e.g.df["d"].dt.date) as dates,datetime64as timestamps, strings as text. Convert before emitting if you want a date rather than a timestamp. - DreamSheets records which tables and tiles a run read, and draws lineage arrows from the sources into the script card and out to its outputs.
- Errors show only your script's lines — library and runtime internals are stripped, so the message always points at something you wrote.
print()/cat()output is captured and shown.
Trust and safety
Scripts run with your interpreter's full capabilities (files, network — whatever your Python or R can do). That's the point — and the risk. So:
- Scripts never auto-run, ever. Opening a document executes nothing.
- The first time you run a script from a document you loaded, DreamSheets asks whether you trust this document's scripts; the answer resets when you open a different document.
- Scripts read the open document's tables and tile values only, and write back only through
emit_table/emit_tile— each run's output is a normal undoable change. - A runaway script is stopped after two minutes rather than wedging the app.
Scripts vs. the alternatives
- Reshaping data with joins and grouping → a query table is simpler and refreshable in place.
- A derived value per row → a calculated column.
- Iterative algorithms, calling libraries (stats, ML, requests), custom cleanup, simulation → a script. Reach for R when the analysis you want is a CRAN package; reach for Python when it's a pandas/scikit one.