Agent Workflow
The edit, save, recalc, inspect, fix loop for agents that change .xlsx files with openpyxl or another editor.
Keep the editing tool you already use. Formualizer only fills in formula results, so the loop is:
- Edit inputs or formulas with openpyxl (or any editor) and save.
- Recalc last:
formualizer recalc file.xlsx --json. Capture stdout and the exit code. - Read the JSON: branch on the exit code and
status, then inspecterror_cellsanderrors. - Fix and repeat: if errors are unexpected, fix inputs or formulas, save, and recalc again.
- Read values with a cached-value reader such as openpyxl
data_only=True, without saving that view.
Recalc must be the last writing step
Formula errors such as #DIV/0! are calculated results and exit 0; decide whether they are expected. Exit 2 means the workbook is outside the supported subset: do not retry the same file and never claim its values were recalculated. Install options are on the overview.
The sections below are maintained in the repository's agent guide.
Python edit/recalc/read loop
After installing openpyxl and a CLI-enabled native formualizer wheel, run this in a directory where file.xlsx may be created or replaced:
import json
import subprocess
import sys
from openpyxl import Workbook, load_workbook
# Edit and save with your usual tool. Repeat this phase to fix reported errors.
wb = Workbook()
ws = wb.active
for row, value in enumerate([10, 20, 30], start=1):
ws.cell(row, 1, value)
ws["B1"] = "=SUM(A1:A3)"
ws["C1"] = "=SEQUENCE(3)"
wb.save("file.xlsx")
wb.close()
p = subprocess.run(
[sys.executable, "-m", "formualizer", "recalc", "file.xlsx", "--json"],
capture_output=True, text=True, check=False,
)
# Launcher/import failures may not produce CLI JSON. Never treat them as success.
if not p.stdout.strip():
raise RuntimeError(p.stderr or "CLI produced no JSON")
result = json.loads(p.stdout)
print(p.returncode, result["status"])
if p.returncode == 2:
raise SystemExit(f"Refused; do not retry: {result['refusal']}")
if p.returncode != 0:
raise SystemExit(f"Stop and diagnose: {p.returncode}: {result['message']}")
assert result["status"] in {"written", "unchanged"}
for error in result["errors"]:
print(error["sheet"], error["cell"], error["error"], error["message"])
for function in result["unknown_functions"]:
print("not implemented:", function["name"], function["cells"])
if result["error_cells"]:
raise SystemExit("Inspect errors, fix inputs/formulas, save, then recalc again")
values = load_workbook("file.xlsx", data_only=True)
print(values.active["B1"].value) # 60
print([values.active[f"C{r}"].value for r in range(1, 4)]) # [1, 2, 3]
values.close() # read only: do not save this workbookTo use the standalone binary instead, replace the subprocess argv with ["formualizer", "recalc", "file.xlsx", "--json"]. Do not use check=True: a refusal or stale check needs its own branch.
Shell loop step
After your editor saves file.xlsx, use this POSIX shell step (requires jq). Repeat the edit/save phase only when inputs or formulas need correction.
rc=0
result=$(formualizer recalc file.xlsx --json) || rc=$?
printf '%s\n' "$result"
case "$rc" in
0) printf '%s\n' "$result" | jq '{status, error_cells, errors, errors_truncated, unknown_functions}'
# Inspect errors; if unexpected, fix inputs/formulas, save and recalc again.
;;
2) printf '%s\n' "$result" | jq '.refusal'
echo 'Refused: do not retry or claim recalculation.' >&2; exit 2 ;;
3) echo 'Check found stale caches: run without --check.' >&2; exit 3 ;;
1|64|130) echo "Stop and diagnose CLI exit $rc." >&2; exit "$rc" ;;
*) echo "Unexpected launcher/process exit $rc." >&2; exit "$rc" ;;
esacBranch on the outcome
| Exit | JSON status | Agent action |
|---|---|---|
| 0 | written, unchanged; current with --check | Computation succeeded. Inspect errors before accepting results. |
| 1 | error | Diagnose I/O, invalid input or engine/internal failure; do not claim updated caches. |
| 2 | refused | Read refusal.feature and refusal.context (what they mean). Do not retry the same unsupported workbook. Leave caches as-is or use another engine; never claim values were recalculated. |
| 3 | stale | --check only: nothing written. Run without --check to publish updates. |
| 64 | error | Correct command-line arguments. |
| 130 | interrupted | Cancellation before publication; nothing written. Resume only when intended. |
Excel table-bearing sheets are supported only within the validated subset described in cache-only XLSX eligibility. When extending a calculated column, write a worksheet formula into every new row: table-level formula metadata alone is refused. Bare table names such as SUM(Table1) mean the data body. In table-bearing workbooks, INDIRECT needs literal text that names no table; cell-sourced INDIRECT text is refused there. [#This Row] is supported only in the data body. Shared formulas whose sheet qualifiers look like cell references (for example 'Q1'! or 'FY2024'!) are refused; write ordinary per-cell formulas instead. Table XML and geometry are never rewritten. There is no automatic fallback engine; branch on explicit refusal rather than publish stale values.
Verify without writing
formualizer recalc file.xlsx --check --jsonExit 0 / current means caches and spill shape are current; exit 3 / stale means they would change. Neither writes a file, even with -o.
Reproducible runs
RAND/RANDBETWEEN are reproducible run to run by default. TODAY/NOW use the host's local time unless you pass --now <RFC 3339 with offset or Z> (and optionally --tz UTC|±HH:MM, --seed <u64>). With --now, output is byte-identical across runs, so --check with the same flags is meaningful for volatile workbooks. Each computed JSON report echoes clock and seed; --now <clock.now> --seed <seed> (plus --tz <clock.timezone> unless it is Local) replays it. See reproducible runs.
Read formula errors
errors contains {sheet, cell, error, message} locations such as Sheet1, B4, #DIV/0!. These are formula results, not tool failures: representable error results can be written successfully with exit 0. Decide whether they are expected; otherwise fix inputs/formulas and repeat the whole edit/save/recalc loop. error_cells is the total count; errors_truncated means some locations were omitted. Use --max-errors N to raise the default 20-location cap (0 lists none); it does not change the total count.
Read message before deciding. It is the engine's reason, or null when the cell has none of its own (an ordinary #DIV/0!, or an error inherited from a precedent):
Unknown function: NAME: the formula calls a function formualizer does not implement, typically an Excel add-in (_xll.EURO,_xll.BDP), a VBA/macro function, or a misspelling. Excel shows#NAME?too when the add-in is missing. formualizer cannot supply those values: fix a misspelling, or decide whether the workbook can be handed off with these cells as#NAME?and say so.unknown_functionslists every such function with its cell count, even whenerrorsis truncated.Undefined name: NAME: the formula uses a name that is not defined. Define it or fix the formula.
A numeric cache that agrees with the computed value to within one unit in the 15th significant digit is left as it is and does not count as a change (see numeric precision). Workbooks set to "precision as displayed" are computed in full precision.
Spills, preservation and safety
- New multi-cell results such as
=SEQUENCE(3)become dynamic-array anchors; validated existing XLDAPR anchors can grow, shrink or collapse.A1#and_xlfn.ANCHORARRAY(A1)read the current spill. - 1×1 limitation: a fresh ordinary formula returning one cell remains scalar: its
A1#reader returns#REF!. An existing validated anchor retains spill identity after collapsing to one cell. Do not infer identity from function names. - Source-declared spill children are generated caches, not independent inputs. Obsolete member caches are cleared while their styles/comments remain. Supported blocked anchors cache
#SPILL!; unsupported spill publication is refused. This subset does not promise full Excel equivalence. - The strict source-preserving path keeps formula text, styles, drawings, names and untouched package content. It changes formula/generated caches and spill geometry, including required dynamic metadata, relationships and worksheet dimensions. It does not rebuild the workbook through an editor.
- Default writes in place, atomically; a true in-place no-op leaves bytes and mtime untouched. Use
-o calculated.xlsxto keep the input separate: the requested output is always published, even with zero changes. If output is the input path, it targets that input. Symlink destinations are refused. - Recalc after every openpyxl save: saving drops formula caches again. Reading with
data_only=Trueis fine, but do not save that values-only view. An openpyxl re-save can also remove dynamic metadata: the next recalc succeeds as a fixed-size CSE array, keeping its original extent. Shorter results pad with#N/A; larger results truncate;A1#readers become#REF!. Use ordinary range readers when this fixed-extent behavior is intended. - Array-condition
IFselects elementwise, including insideSUMand fixed CSE arrays. Scalar branches and singleton axes broadcast; incompatible shapes return#VALUE!. - Don't run concurrent writers on the same file. Atomic replacement is not compare-and-swap protection against another editor.
See cache-only XLSX eligibility, refusals and resource bounds for the supported subset (including fixed-extent array policies and refusals for tables, external links and unsupported names/metadata), and the authoritative CLI reference for JSON fields, counters and publication details. Limits are bounded by default and have no CLI tuning flags.
A copyable agent skill teaches the same workflow without requiring these docs alongside it.
Volatile formulas are recomputed with one request clock sample and the configured RNG seed on each source-recalc run. Their dependents must agree with that same evaluation; without --now, byte-identical reruns are conditional on an unchanged clock sample, and --check reports stale when it differs. SUBTOTAL/AGGREGATE ranges intersecting stored hidden row or active-filter rows are refused because source row visibility is not hydrated; zero-height rows, rows grouped under a collapsed outline and zeroHeight sheets count as hidden. Dynamic reducer ranges, range operators over functions or names, and LET/LAMBDA-bound reducer arguments are refused when their hidden-row intersection cannot be proved.
Recalc CLI
Recalculate the cached formula values in an .xlsx after openpyxl or another tool edits it, from Python, npm, Cargo or a prebuilt binary.
CLI Reference
Every formualizer recalc flag, in-place and -o publication, --check, reproducible --now/--tz/--seed runs, exit codes 0/1/2/3/64/130, the formualizer.recalc/1 JSON schema and cancellation.