diff --git a/crates/engine/src/cmd/extra/data.rs b/crates/engine/src/cmd/extra/data.rs
--- a/crates/engine/src/cmd/extra/data.rs
+++ b/crates/engine/src/cmd/extra/data.rs
@@ -82,11 +82,19 @@ pub(super) fn from_csv(s: &mut Session, p: &Json) -> Result<Json> {
             (u.start.row..=u.end.row)
                 .map(|r| {
                     (u.start.col..=u.end.col)
-                        .map(|c| match sh.value(CellRef::new(r, c)) {
-                            Value::Number(n) => json!(n),
-                            Value::Bool(b) => json!(b),
-                            Value::Empty => Json::Null,
-                            v => json!(v.display()),
+                        .map(|c| {
+                            let at = CellRef::new(r, c);
+                            // A formula field goes in as typed input, so the new sheet holds the same formula
+                            // (the parsed workbook is never recalculated: its value is still empty).
+                            if let Some(f) = sh.cell(at).and_then(|cell| cell.formula.as_ref()) {
+                                return json!(format!("={}", f.text));
+                            }
+                            match sh.value(at) {
+                                Value::Number(n) => json!(n),
+                                Value::Bool(b) => json!(b),
+                                Value::Empty => Json::Null,
+                                v => json!(v.display()),
+                            }
                         })
                         .collect()
                 })
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -2434,6 +2434,31 @@ fn ods_import_never_reuses_source_as_save_target() {
 }
 
 /// Open a `;` CSV by path (a double-click), edit, Save: the file keeps its `;` separator.
+/// A CSV written by a script with totals as formulas opens computed, as in Excel, instead of
+/// showing the formula text until the cell is re-entered; what doesn't parse stays text.
+#[test]
+fn csv_formulas_open_computed() {
+    let text = "Item,Amount\r\nRent,800\r\nFood,300\r\nTotal,=SUM(B2:B3)\r\nMean,\"=AVERAGE(B2:B3)\"\r\nLocal,=SOMMA(B2;B3)\r\nSign,=\r\n";
+    let mut s = s();
+    s.execute("file.open", json!({"name": "budget.csv", "base64": crate::io::base64_encode(text.as_bytes())})).unwrap();
+    let v = s.execute("sheet.read", json!({"range": "B4:B7"})).unwrap();
+    assert_eq!(v["values"], json!([[1100.0], [550.0], ["=SOMMA(B2;B3)"], ["="]]));
+    let f = s.execute("sheet.read", json!({"range": "B4:B5", "formulas": true})).unwrap();
+    assert_eq!(f["values"], json!([["=SUM(B2:B3)"], ["=AVERAGE(B2:B3)"]]));
+}
+
+/// Data › From Text/CSV reads the fields into a new sheet: a formula field arrives as the same
+/// formula, computed there, not as an empty cell.
+#[test]
+fn csv_import_into_a_sheet_keeps_formulas() {
+    let mut s = s();
+    s.execute("data.fromTextCsv", json!({"text": "Item,Amount\nRent,800\nFood,300\nTotal,=SUM(B2:B3)\n", "sheet": "Budget"})).unwrap();
+    let v = s.execute("sheet.read", json!({"sheet": "Budget", "range": "A4:B4"})).unwrap();
+    assert_eq!(v["values"], json!([["Total", 1100.0]]));
+    let f = s.execute("sheet.read", json!({"sheet": "Budget", "range": "B4", "formulas": true})).unwrap();
+    assert_eq!(f["values"], json!([["=SUM(B2:B3)"]]));
+}
+
 #[test]
 #[cfg(not(target_arch = "wasm32"))]
 fn saving_an_opened_semicolon_csv_keeps_semicolons() {
diff --git a/crates/xlsx/src/csv.rs b/crates/xlsx/src/csv.rs
--- a/crates/xlsx/src/csv.rs
+++ b/crates/xlsx/src/csv.rs
@@ -4,7 +4,7 @@ use std::sync::Arc;
 
 use gridcraft_core::parse::parse_input;
 use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, Value};
-use gridcraft_model::{Cell, NumFmt, Sheet, StyleId, Workbook};
+use gridcraft_model::{Cell, Formula, NumFmt, Sheet, StyleId, Workbook};
 
 use crate::{CsvOptions, IoError};
 
@@ -275,7 +275,9 @@ pub fn read_csv(bytes: &[u8], opts: &CsvOptions) -> Result<Workbook, IoError> {
                 continue;
             }
             let pos = CellRef::new(row, col as u32);
-            let cell = if opts.parse_values {
+            let cell = if let Some(cell) = opts.parse_values.then(|| formula_cell(&f)).flatten() {
+                cell
+            } else if opts.parse_values {
                 let p = parse_input(&f, opts.date_system);
                 let style = match p.format {
                     Some(code) => match fmt_cache.iter().find(|(c, _)| *c == code) {
@@ -302,6 +304,17 @@ pub fn read_csv(bytes: &[u8], opts: &CsvOptions) -> Result<Workbook, IoError> {
     Ok(wb)
 }
 
+/// Like Excel, a field that starts with `=` is a formula (`=SUM(B2:B9)`), computed when the
+/// workbook opens; one that doesn't parse (`=SOMMA(B2;B9)`, a lone `=`) stays text.
+fn formula_cell(field: &str) -> Option<Cell> {
+    let body = field.strip_prefix('=')?;
+    if body.trim().is_empty() {
+        return None;
+    }
+    let expr = gridcraft_formula::parse(body).ok()?;
+    Some(Cell::formula(Formula::from_expr(expr)))
+}
+
 /// Writes the sheet as CSV from A1 to the end of its used range, so leading empty columns and rows
 /// stay as empty fields and every cell keeps its position (CRLF line ends, UTF-8 without BOM). `cell_text` gives
 /// each cell's field text: the caller decides how values look (Save As CSV writes them as
diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md
--- a/docs/file-format-parity.md
+++ b/docs/file-format-parity.md
@@ -1,6 +1,6 @@
 # File-format parity
 
-> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-21 · **Change:** minor (.xls dialog sheets are skipped instead of read as worksheets; `.xls` import reads each sheet's display options from WINDOW2: right-to-left, gridlines, headings, zeros, formulas; XLSX text boxes and shapes keep their paragraphs' right-to-left direction on save; CSV/TSV/TXT can open with every field kept as text via `file.open {csvAsText}`; formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened; XLSX PivotTables read and write `rowGrandTotals`/`colGrandTotals` the way Excel does, so a table saved with only one grand total opens with the right one; HTML export keeps right-to-left; CSV and text import detects likely Windows-1256 Arabic when invalid UTF-8 has Arabic byte runs; other invalid UTF-8 remains Windows-1252; HTML export places pictures and charts, as inline SVG, at their anchors) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-27 · **Change:** minor (CSV fields starting with `=` open as formulas, as in Excel) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
 
 Every format Excel opens or saves, with GridCraft's read and write support, fidelity and tests.
 Part of [target-app-parity.md](target-app-parity.md). **File formats: ~50% ready, 80–120 h**
@@ -66,9 +66,9 @@ values) is still dropped.
 
 | Format | Excel | GridCraft read | GridCraft write | Notes |
 |---|---|---|---|---|
-| `.csv` UTF-8 / Windows / Mac / MS-DOS | R/W | yes (UTF-8 and UTF-16 BOMs, UTF-8, likely Windows-1256 Arabic detection, otherwise Windows-1252 fallback; delimiter detection) | yes (UTF-8 without BOM, CRLF; a file opened as CSV is saved with the separator it was read with, so a `;` file stays `;`; otherwise `,`) | Excel writes the **displayed** values; we write raw values. No encoding choice on save. Re-import of a single-column export splits text (#127) A used range that would write more than 512 MiB (two far-apart cells span the whole rectangle) is refused with an error instead of being expanded (#528). |
+| `.csv` UTF-8 / Windows / Mac / MS-DOS | R/W | yes (UTF-8 and UTF-16 BOMs, UTF-8, likely Windows-1256 Arabic detection, otherwise Windows-1252 fallback; delimiter detection; a field starting with `=` is a formula, computed on open, or text when it doesn't parse) | yes (UTF-8 without BOM, CRLF; a file opened as CSV is saved with the separator it was read with, so a `;` file stays `;`; otherwise `,`) | Excel writes the **displayed** values; we write raw values. No encoding choice on save. Re-import of a single-column export splits text (#127) A used range that would write more than 512 MiB (two far-apart cells span the whole rectangle) is refused with an error instead of being expanded (#528). |
 | `.txt` tab-delimited, Unicode text | R/W | yes (`.txt`, `.tsv`, `.tab`; likely Windows-1256 Arabic detection for invalid UTF-8) | yes | Text Import Wizard options partly via Text to Columns |
-| `.csv` UTF-8 / Windows / Mac / MS-DOS | R/W | yes (UTF-8 and UTF-16 BOMs, UTF-8, likely Windows-1256 Arabic detection, otherwise Windows-1252 fallback; delimiter detection) | yes (UTF-8 without BOM, CRLF) | Writes the **displayed** values like Excel (number formats applied, formula results, never column-width `#####`). No encoding choice on save. Re-import of a single-column export splits text (#127) A used range that would write more than 512 MiB (two far-apart cells span the whole rectangle) is refused with an error instead of being expanded (#528). |
+| `.csv` UTF-8 / Windows / Mac / MS-DOS | R/W | yes (UTF-8 and UTF-16 BOMs, UTF-8, likely Windows-1256 Arabic detection, otherwise Windows-1252 fallback; delimiter detection; a field starting with `=` is a formula, computed on open, or text when it doesn't parse) | yes (UTF-8 without BOM, CRLF) | Writes the **displayed** values like Excel (number formats applied, formula results, never column-width `#####`). No encoding choice on save. Re-import of a single-column export splits text (#127) A used range that would write more than 512 MiB (two far-apart cells span the whole rectangle) is refused with an error instead of being expanded (#528). |
 | `.txt` tab-delimited, Unicode text | R/W | yes (`.txt`, `.tsv`, `.tab`; likely Windows-1256 Arabic detection for invalid UTF-8) | yes (displayed values) | Text Import Wizard options partly via Text to Columns |
 | `.prn` formatted text (space-delimited) | R/W | no | no | |
 | `.dif`, `.slk` (SYLK) | R/W | no | no | Small, legacy |
@@ -110,6 +110,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-27 | minor | CSV import: a field starting with `=` is a formula computed on open, as in Excel (it was kept as text until the cell was re-entered); one that doesn't parse stays text |
 | 2026-10-21 | minor | HTML (Save as Web Page) writes pictures (`data:` URI `<img>`) and charts (inline SVG from the chart crate's drawing primitives, text included) at their anchors; the table uses the sheet's column widths and row heights when the sheet has either, and output for sheets without pictures or charts is unchanged |
 | 2026-10-20 | trivial | XLSX shared strings: Japanese phonetic runs (ふりがな, `<rPh sb="…" eb="…">` with `<phoneticPr>`) are read into the workbook and written back next to the string with the same text, so an open + save no longer deletes them; bad offsets and readings are dropped with a warning, a `<phoneticPr>` without runs is kept on its own, and when duplicate `<si>` items share a text but not a reading only the first reading is written (reported on open) (`phonetic_tests.rs`) |
 | 2026-10-20 | minor | A workbook theme's major/minor per-script typefaces (`<a:font script="Arab" typeface="…"/>`, also Hebr, Jpan and the rest) are read and written back in file order after `<a:cs>`; they were dropped before. A hostile file is held to 64 entries of 256 characters |
