Import & export
CSV, JSON, Excel round-tripping, the Export dialog, external databases, PDF, the .dsheet file format, autosave, and sharing.
CSV
- Import: File → Import CSV… (also on the welcome screen). Column types are inferred from the data, with care for the classics — codes with leading zeros stay text.
- Export: per table, from the table menu → Export… and pick CSV — see Export.
JSON
File → Import JSON… turns a JSON file of records — the shape an API returns — into a table. The file's root should be an array of objects (a wrapper one level in, like {"data": […]}, is found automatically); each object becomes a row, and column types are inferred the same way CSV import does, so numbers, dates and booleans arrive as their real types.
JSON isn't flat, so the importer flattens it honestly:
- Nested objects become dot-path columns —
prices.usd— up to three name parts deep; anything deeper is kept as JSON text. - Arrays of plain values join with commas (
["W","U"]→W,U), which is the form you can filter on. - Arrays of records keep their JSON text, for a calc column or script to pick apart.
- Records don't all need the same keys: columns are the union across all rows, missing values come in empty, and every lossy choice is listed in the import notes rather than happening quietly.
For data straight from an API — or too big for a file — a script can fetch it and emit a table directly.
From a URL
File → Import from URL… takes a link instead of a file: paste the address of a CSV or JSON file and DreamSheets downloads it and imports it exactly as the two commands above would — same type inference, same import notes. The table is named after the file the server sends (or the link's last segment).
It also understands Socrata open-data pages — the portals behind data.sf.gov, data.cityofnewyork.us, data.cityofchicago.org and hundreds of other governments. Paste the dataset page you have open (…/Supplier-Contracts/cqi5-hm2d/about_data) and the whole dataset arrives through the portal's own CSV export. Any other web page is refused with a message rather than imported as HTML.
An imported URL is a snapshot. For a table you can refresh — or to pull a filtered, grouped slice instead of the whole dataset — add the portal as a database connection. Desktop app only.
Into an existing table
File → Import into Table… (also on a table's right-click menu) adds a CSV, JSON or Excel file's rows to a table you already have — and only the rows it doesn't already hold. It's built for recurring exports that overlap: this month's bank statement, a weekly CRM or inventory pull, a log.
- Map the columns. File columns match table columns by name; pick a different one (or don't import) for any that differ. The mapping is remembered on the table, so next month's file needs no mapping at all.
- Choose what makes a row the same — by default, every mapped column. Checking just a transaction ID, say, matches on that alone.
- Choose a mode. Add new rows only, or Add new rows and update matching rows — which rewrites only the mapped columns, so a category or note you typed by hand is never touched.
- Preview. The counts — "187 new rows will be added, 25 already present" — come before anything changes, and the import is one step you can undo.
Rows are compared as values of the table's column types, so 5, 5.0 and 5.00 are the same amount, and 1/5/2026 is the same day as 2026-01-05. Repeats count: if the file has three identical $5 coffees on one day and the table already has two, one is added — two identical purchases are two purchases. Matching is exact otherwise: a description that changed between exports is a new row, which is what the preview is for.
Calculated columns fill themselves and linked query tables get their rows from their query, so neither can be imported into. Desktop app only.
Excel import
File → Import Excel… brings in an .xlsx workbook: one tab per worksheet, one table per sheet's data, with types, styles, and charts reconnected to the imported tables.
Importing always starts with a choice: the standard import works the layout out itself and sends nothing anywhere, while the GenAI import asks your connected AI provider to read the workbook's structure first — useful for creatively laid-out sheets (several tables per sheet, captions between blocks, dashboards). Only a structural sketch is sent: sheet names, text labels and formulas — numbers, dates and booleans go as placeholders, never their values. No provider connected yet? The same dialog offers to open AI Settings so you can connect one on the spot.
The browser editor can import Excel too — File → Import Excel… uploads the workbook, converts it in the cloud, and opens the imported copy as a new Drive document (as does dropping an .xlsx straight onto the Drive).
The interesting part is formulas. DreamSheets translates Excel formulas into calc columns and summary rows — but a translation is never trusted blind. Every translated column is checked against the workbook's own cached values, row for row; only a translation that provably reproduces Excel's numbers is promoted to a live formula. Everything else is kept as static data — imported values are never wrong, just sometimes not yet live.
For everything kept static:
- The original Excel formula is preserved on the column's description, so nothing is lost.
- An "Import notes" tile is added listing every column kept static and why.
- The import report's Translate with AI button can take a second pass: an AI model proposes DreamSheets formulas (building helper query tables where needed), and each proposal is verified against the cached values with the same oracle before it's committed — a failed attempt changes nothing. See AI assistance.
Isolated one-off formula cells become per-cell formulas the same verified way, and KPI-style summary sheets can import as tiles rather than tables.
Export
One dialog behind two doors: Export… in a table's menu exports that table, and File → Export… exports the whole document. Pick a format, then Copy it to the clipboard or Save file… (the dialog remembers your last format, so the second table is two clicks):
- Per table — CSV · Excel (XLSX) (the table as a workbook) · Markdown (a pipe table for docs and READMEs) · LaTeX (a booktabs fragment to paste into a paper) · Word / HTML (paste into Word with formatting intact) · Parquet (columnar data for pandas, DuckDB, Spark).
- Whole document — Excel (XLSX) and AI Document (.dsdoc) (paste into an AI chat alongside the authoring guide — see Connecting AI tools).
An APA style toggle applies to the LaTeX and Word/HTML formats, so a results table lands in a paper already formatted to APA's rules. A table exports what you're looking at: active column filters are respected. In the browser editor the button is Download, and the two binary formats (XLSX, Parquet) aren't offered.
What an Excel export carries
Exporting the document as Excel (XLSX) writes a real workbook: one worksheet per tab, tables laid out top-to-bottom, number formats carried over, and:
- Live formulas: calc columns and summary aggregates export as real Excel formulas — including cross-table references, which become cross-sheet ranges. Anything that can't translate faithfully (custom functions, DreamSheets-only functions) exports as its computed value instead — always correct, never guessy.
- Tiles export as labeled value cells placed next to the table they read from.
- Charts export as native Excel charts pointing at the exported ranges.
- Conditional formats export where Excel has an equivalent (rules, 2-color scales, data bars).
- Formatting — font, size, bold/italic/strikethrough, alignment, text overflow (wrap and shrink-to-fit both map to Excel's own) and colors — exports per cell, resolved through the same column-then-cell cascade the grid draws, so what you see is what the workbook gets.
External databases
File → Database Connections… manages connections to SQLite, DuckDB / Parquet and CSV (file paths), PostgreSQL and MySQL/MariaDB (server URLs), SQL Server (an ADO connection string — the Server=…;Database=…;User Id=…;Password=… form you already have from SSMS or a .NET config), the BigQuery and Snowflake warehouses, and Socrata open-data portals (below). Pull data in via Insert → Table from Database… — a query table whose SQL runs on that database, with the result copied into your document.
Anything that speaks one of those wire protocols works without being listed separately: Redshift, Supabase, Neon, CockroachDB, Timescale, AlloyDB connect as PostgreSQL, and PlanetScale, Aurora MySQL, TiDB, SingleStore as MySQL.
A DuckDB connection is a file rather than a server, and it takes two shapes. A .duckdb database opens read-only and its tables work as you'd expect. A `.parquet`, `.csv` or `.json` file — or a glob like sales/*.parquet — is read directly and appears as a single table named after the file (Q1 sales.parquet → Q1_sales), so you can SELECT, join and aggregate over a data file with no import step and no server at all.
A CSV file connection is for the file you get again every week. Its table is named after the connection, not the file — name it Weekly Sales and you query Weekly_Sales — so when next week's export arrives as sales-0917.csv, the connection's Replace file… button points it at the new file and refreshes every table in the open document that reads it. Queries built on those tables follow on their own, like any linked query. Overwrote the file in place instead? Refresh on the same row reloads without picking anything. The data is copied into your document either way, so the .dsheet still opens without the CSV. Renaming the connection renames its table, and the queries in the open document that read it are rewritten to the new name (one undo step). Other documents that use the connection aren't open, so they aren't rewritten — edit their queries to the new name.
The shape of the feature in one line: DreamSheets is a read-only importer, not a database client. It runs the SELECT you write and materializes a snapshot. Knowing exactly where that line falls saves surprises:
What it does
- Runs your SQL on the server, in that server's own dialect — the embedded DuckDB engine is not involved, so
information_schema, vendor functions and server-side indexes are all in play. - Types the result the way CSV import does: integers, floats, booleans, and dates — a
DATE/TIMESTAMPcolumn arrives as a real date column with a date format, not text, so it sorts chronologically and works in formulas. PostgreSQLNUMERIC, MySQLDECIMALand SQL ServerDECIMAL/MONEYarrive as numbers. Anything else (UUID, JSON, arrays, enums, intervals, XML) comes through as text rather than failing the import. Binary blobs import as empty cells. A SQL ServerDATETIMEOFFSETlands as its UTC instant — the row model carries no time zone. - Shows you the schema. "Test" in the connections dialog lists the tables; in the query editor the autocomplete offers that database's tables and columns as you type. PostgreSQL tables outside
public— and SQL Server tables outsidedbo— are listed schema-qualified (analytics.orders), because that's how you have to query them. - Refreshes on demand — Refresh query in the table's menu re-runs the stored SQL against the live database, and Edit query… changes the SQL in place while keeping the connection.
What it doesn't
- No writes, ever. No
INSERT/UPDATE/DELETE/DDL, no write-back of edits you make to the imported table — those live in your document only. SQLite and.duckdbfiles are opened read-only, and the Postgres/MySQL sessions ask the server to refuse writes; on an old server that request is ignored, and SQL Server has no session-level read-only mode at all, so the real guarantee is connecting as a read-only database user. Do that. - No automatic refresh. An imported table never re-runs on its own — not on a timer, not when the document opens, and not when you edit a table that feeds it (that cascade applies to internal query tables only). It is a snapshot until you press Refresh.
- Results over 100,000 rows arrive read-only. At or under 100,000 rows the result is an ordinary, editable table. A bigger result still imports in full — from PostgreSQL, MySQL, SQLite and DuckDB files it streams into a read-only Big table, stored compactly inside the document with no size ceiling — and when you need an editable table, filter or aggregate it into a smaller one (the table menu's Load subset… starts that query for you). Two exceptions do error with "narrow it": SQL Server holds the whole result in memory while fetching and stops at 500,000 rows, and plugin connectors stop at 100,000. Prefer not to copy the rows at all? Under Linked, choose Read rows live from the database when creating the table (PostgreSQL, MySQL, SQL Server, SQLite and DuckDB files) — the document stores only the query, and rows are fetched from the database as you view them.
- No AI text-to-SQL for external sources. The "Describe the result" generator writes SQL against this document's tables; it's hidden when the source is a database.
- One statement per query, returning one result set. No stored-procedure calls with multiple results, no query parameters, no
EXPLAIN-style client commands. - No Oracle, MongoDB or Databricks built in. Anything else that answers over HTTPS belongs in a connector plugin — see below.
Socrata open-data portals
Hundreds of governments publish their data on Socrata — data.sf.gov, data.cityofnewyork.us, data.cityofchicago.org, data.ny.gov and many more. Add one as a connection (Database: Socrata open data) by pasting the URL of the dataset page you have open, e.g. https://data.sf.gov/City-Management-and-Ethics/Supplier-Contracts/cqi5-hm2d/about_data. That pins the dataset; a bare portal address (https://data.sf.gov) connects the whole portal instead. The data is public, so no password is involved — optionally add ;token=<app token> (free from the portal's developer settings) to lift the rate limit Socrata applies to anonymous callers.
Once connected it behaves like any other source:
- Test lists the dataset's columns (or the portal's catalog), and the query editor autocompletes them.
- Insert → Table from Database… (Alt+D, or Create table from database… on the canvas's right-click menu) runs your query on the portal, so a filter or
GROUP BYover a million-row dataset downloads only its answer. Queries are written in SoQL, Socrata's SQL dialect —SELECT,WHERE,GROUP BY,HAVING,ORDER BY,LIMIT, and functions likedate_trunc_ym(). A connection to one dataset calls its table by the connection's name — name the connection City Contracts and you querycity_contracts; a whole-portal connection names each dataset after its title. The dataset's id (the code at the end of its URL) always works too:
``sql SELECT department, SUM(agreed_amt) AS total FROM city_contracts WHERE term_end_date > '2026-01-01' GROUP BY department ORDER BY total DESC ``
With a single pinned dataset the FROM can be left out, and an imported table you don't name takes the connection's name. A query without a LIMIT fetches every row (Socrata's own default is 1,000, so DreamSheets asks for all of them explicitly).
- Types come from the portal's schema: number columns arrive as numbers, checkboxes as booleans, and date columns as real dates — midnight-only timestamps as dates, the rest as timestamps. Locations and other structured values arrive as JSON text.
- Refresh query re-runs the SoQL against the live dataset, which is the point for data that updates daily.
Everything under What it doesn't above applies too: it is read-only, it never refreshes on its own, and a result over 100,000 rows lands read-only.
Connectors from plugins
A plugin can contribute its own database kind, and it is a peer of the five above rather than a bolt-on. Once installed, the kind appears in the same Database Connections… dropdown, its connection string is stored alongside the others (in the OS credential store, never in a .dsheet), Test lists its schema and feeds the query editor's autocomplete, and a table imported from it is a real linked query table with Refresh query and Edit query…. Results are typed by the app, not the plugin — the same integer/float/date inference. One real limit: plugin results are capped at a hard 100,000 rows — they can't use the Big-table path the built-in engines get, so a larger result is an error, never a truncation.
This is how Databricks — or any other source that answers over HTTPS — is meant to be reached. The bundled Sample Connector shows the whole flow against in-memory data; writing one is documented at `connectors.register`.
One consequence worth knowing: a connection provided by a plugin only works while that plugin is loaded. Disable or remove it and the connection says so plainly, and its tables keep their last refreshed data — the same behavior as opening a .dsheet on a machine that lacks the connection.
Credentials and transport
Connections live in the app's configuration, never inside the document — a shared .dsheet references a connection by id only, and a machine without that connection just sees the last refreshed data as a static snapshot. The connection string itself — the part that carries a password — is kept in your operating system's credential store (Windows Credential Manager, macOS Keychain, or the Linux Secret Service), not in a config file. On a system with no credential store it falls back to the config file, and a connection saved in plain text by an older release moves into the credential store the next time it loads. Either way, a read-only database user with the narrowest possible grants is the right thing to connect as.
Transport is encrypted by default, and a server that can't negotiate TLS fails with an error naming the opt-out — never a silent fallback to plain text:
| Engine | Encryption |
|---|---|
| SQLite | N/A — a local file, opened read-only |
| DuckDB / Parquet | N/A — a local file (a .duckdb database is opened read-only) |
| CSV file | N/A — a local file |
| PostgreSQL | TLS required by default, with certificate validation. A provider that signs with its own certificate authority (Supabase does) needs that CA named: download it and add sslrootcert=<path to the file> to the URL. Add sslmode=disable for a legacy server on a trusted network; any sslmode= you set explicitly is honoured as written |
| MySQL / MariaDB | TLS required by default, with certificate and hostname validation; the same sslmode=disable opts out |
| SQL Server | Encrypted by default; TrustServerCertificate=true skips certificate validation — self-signed certificates only, and Test warns when it sees it |
And a reminder from above: SQL Server has no session-level read-only mode, so there the read-only guarantee is the login's permissions — nothing else.
Print and PDF
File → Print / Save as PDF… (Ctrl+P) prints the canvas using the system print dialog, which includes save-to-PDF, paper size, orientation, and scale.
The .dsheet file
A .dsheet is a zip archive: JSON for the document structure and formulas, plus one parquet file per table for row data. That buys three things:
- Speed — a million-row document opens in about a second.
- Robustness — the format is designed to survive partial writes, and saves carry integrity hashes.
- Scriptability —
unzip,jq, andduckdbcan read your own files. No lock-in.
Reading a .dsheet from Python
No DreamSheets install needed — document.json names the tables and the parquet files carry the rows under the real column names:
import zipfile, json, io, pandas as pd
z = zipfile.ZipFile("Budget.dsheet")
doc = json.loads(z.read("document.json"))
for t in doc["tables"]:
df = pd.read_parquet(io.BytesIO(z.read(f"data/{t['tab_id']}/{t['id']}.parquet")))
print(t["name"], list(df.columns), len(df))Or download dsheet.py — one file, pandas + pyarrow — and dsheet.load("Budget.dsheet") gives you a DataFrame per table by name, plus each table's calculated-column formulas and every tile. Polars, DuckDB, and R's arrow read the same parquet files.
Two things to know. Each parquet carries two system columns, _rowid and _insert_at, that the app uses for identity — drop them. And calculated columns, formula tiles and totals rows are computed by the app, not stored: you get their formula text (from formulas.json), not their values. Query tables are stored, so their results are readable. The format is read-only from outside: to write a document, produce dsdoc JSON and import it. A version archive fetched straight from cloud storage stores table data by reference and won't open this way — dsheet.py says so rather than returning empty tables; download the document from your Drive instead.
Autosave and recovery
While a document is dirty, DreamSheets writes background recovery snapshots (debounced to stay out of your way; only changed tables are re-encoded; the window close flushes a final one). After a crash or force-quit, the next launch lists recoverable documents with Recover / Discard — recovery restores the document with its original file path and marks it unsaved, so Ctrl+S completes the story. A successful save cleans the snapshot up.
Sharing a document
File → Share… invites people by email, each as an Editor or a Viewer, or hands out one link that grants the same — the same share you get from the Share button in your Drive. Sharing means the document lives in DreamSheets Cloud, so a local one is saved there first. From then on, File → Sync Changes streams everyone's edits live. A document you never share performs no networking at all.
Getting a document to an AI
Two clipboard-based paths exist for working with any external chat AI, no API key required — see Connecting AI tools: AI → Copy Prompt for AI and AI → Import AI Output….