xlsx
Skill directory
load_skill returned a path field — the sandbox directory holding this
skill's scripts, templates, and references. Use it verbatim; never construct
the path yourself:
bashexport SKILL_DIR="<the `path` value from load_skill>"
Handle the request in this conversation — don't delegate to subagents: edit
integrity depends on one agent owning the file. Write output files to the
working directory, never into $SKILL_DIR.
Choosing the write strategy: openpyxl vs XML surgery
Two ways to write an .xlsx, each with a job. Pick by one question: does an
input file already exist whose non-cell content must survive?
| Situation | Strategy | Why |
|---|---|---|
| CREATE from scratch (no source file) | openpyxl (default) | Nothing to corrupt. openpyxl makes charts, conditional formatting, cover pages, and rich styling trivial — all painful in raw XML. |
| EDIT / FIX an existing file | XML surgery (required) | openpyxl load_workbook() → save() silently drops VBA, pivot tables, sparklines, slicers, and some charts. Unpack → edit XML → pack changes only the nodes you touch. |
Gray zone — a financial model built from scratch: openpyxl works and is
faster. Reach for XML surgery only when you need byte-level determinism or
precise styles.xml index control (see references/format.md).
The one absolute rule: never openpyxl.load_workbook(existing.xlsx) then
save() — that round-trip is what corrupts. openpyxl is safe only on a
workbook you created in the same script.
openpyxl writes formula strings but no cached results, so an openpyxl-created
model (one that uses formulas) MUST be recalculated (libreoffice_recalc.py) and
checked (formula_check.py) before delivery — same gate as the XML path. A report
that writes computed values directly needs neither step.
Report vs Model: do you need formulas at all?
The Formula-First rule below applies to models — workbooks the user will keep editing, where they change inputs/assumptions and derived cells must recompute. Most "analyze this data and give me a spreadsheet" requests are reports: the numbers are already final, computed by pandas/openpyxl. Forcing live formulas + a LibreOffice recalc onto a report adds a slow, fragile external dependency for zero benefit — the data already knows the answer, and recalc can hang or silently skip cells.
| Deliverable type | Use formulas? | Recalculate? | Why |
|---|---|---|---|
| Report (analysis output, point-in-time snapshot) | No — write the computed value directly | No | Values are final; charts read the values. Zero external dependency, instant, always correct. |
| Model (user will change inputs/assumptions) | Yes — every derived cell is a live formula | Yes (mandatory) | User edits inputs → other cells must recompute in Excel/WPS. |
Decide with one question: Will the user open this file and change numbers, expecting other cells to update? If yes → Model (formulas + recalc). If no → Report (write values). When unsure, a report is the safer default — it always opens correctly with no recalc step.
- Report: set each derived cell to its already-computed number (never a formula).
openpyxl:
ws["C2"] = round(revenue, 2). XML:<c r="C2"><v>12345.67</v></c>(no<f>). Nolibreoffice_recalc.pystep. Runformula_check.pyonly when you edited an existing file (to catch XML/structure issues), not to check formulas. - Model: follow the Formula-First rules below; recalc is mandatory and is the final step before delivery.
Task Routing
| Task | Method | Guide |
|---|---|---|
| READ — analyze existing data | xlsx_reader.py + pandas | references/read-analyze.md |
| CREATE — new xlsx from scratch | openpyxl (default), or XML template for byte-level control | references/create.md + references/format.md |
| EDIT — modify existing xlsx | XML unpack→edit→pack | references/edit.md (+ format.md if styling needed) |
| FIX — repair broken formulas in existing xlsx | XML unpack→fix <f> nodes→pack | references/fix.md |
| VALIDATE — check formulas | formula_check.py | references/validate.md |
READ — Analyze data (read references/read-analyze.md first)
Start with xlsx_reader.py for structure discovery, then pandas for custom analysis. Never modify the source file.
Formatting rule: When the user specifies decimal places (e.g. "2 decimal places"), apply that format to ALL numeric values — use f'{v:.2f}' on every number. Never output 12875 when 12875.00 is required.
Aggregation rule: Always compute sums/means/counts directly from the DataFrame column — e.g. df['Revenue'].sum(). Never re-derive column values before aggregation.
CREATE — new file (read references/create.md + references/format.md)
Default: build with openpyxl — it unlocks charts, conditional formatting,
cover pages, and styling with far less effort than raw XML. Alternative for
byte-level determinism / precise style-index control: copy
templates/minimal_xlsx/ → edit XML directly → pack with xlsx_pack.py.
Either way, the strategy depends on Report vs Model (see that section):
- Model (user will edit inputs): every derived value MUST be a live Excel formula
(
=SUM(B2:B9)/<f>SUM(B2:B9)</f>), never a hardcoded number. Apply font colors performat.md. Recalculate +formula_check.pybefore delivery. - Report (final snapshot): skip formulas entirely — write the computed values directly and deliver immediately. Charts read the values; no recalc needed.
EDIT — XML direct-edit (read references/edit.md first)
CRITICAL — EDIT INTEGRITY RULES:
- NEVER create a new
Workbook()for edit tasks. Always load the original file. - The output MUST contain the same sheets as the input (same names, same data).
- Only modify the specific cells the task asks for — everything else must be untouched.
- After saving output.xlsx, verify it: open with
xlsx_reader.pyorpandasand confirm the original sheet names and a sample of original data are present. If verification fails, you wrote the wrong file — fix it before delivering.
Never use openpyxl round-trip on existing files (corrupts VBA, pivots, sparklines). Instead: unpack → use helper scripts → repack.
"Fill cells" / "Add formulas to existing cells" = EDIT task. If the input file already exists and you are told to fill, update, or add formulas to specific cells, you MUST use the XML edit path. Never create a new Workbook(). Example — fill B3 with a cross-sheet SUM formula:
bashpython3 $SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/ # Find the target sheet's XML via xl/workbook.xml → xl/_rels/workbook.xml.rels # Then use `edit` to add <f> inside the target <c> element: # <c r="B3"><f>SUM('Sales Data'!D2:D13)</f><v></v></c> python3 $SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
Add a column (formulas, numfmt, styles auto-copied from adjacent column):
bashpython3 $SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/ python3 $SKILL_DIR/scripts/xlsx_add_column.py /tmp/xlsx_work/ --col G \ --sheet "Sheet1" --header "% of Total" \ --formula '=F{row}/$F$10' --formula-rows 2:9 \ --total-row 10 --total-formula '=SUM(G2:G9)' --numfmt '0.0%' \ --border-row 10 --border-style medium python3 $SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
The --border-row flag applies a top border to ALL cells in that row (not just the new column). Use it when the task requires accounting-style borders on total rows.
Insert a row (shifts existing rows, updates SUM formulas, fixes circular refs):
bashpython3 $SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/ # IMPORTANT: Find the correct --at row by searching for the label text # in the worksheet XML, NOT by using the row number from the prompt. # The prompt may say "row 5 (Office Rent)" but Office Rent might actually # be at row 4. Always locate the row by its text label first. python3 $SKILL_DIR/scripts/xlsx_insert_row.py /tmp/xlsx_work/ --at 5 \ --sheet "Budget FY2025" --text A=Utilities \ --values B=3000 C=3000 D=3500 E=3500 \ --formula 'F=SUM(B{row}:E{row})' --copy-style-from 4 python3 $SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
Row lookup rule: When the task says "after row N (Label)", always find the row by searching for "Label" in the worksheet XML (grep -n "Label" /tmp/xlsx_work/xl/worksheets/sheet*.xml or check sharedStrings.xml). Use the actual row number + 1 for --at. Do NOT call xlsx_shift_rows.py separately — xlsx_insert_row.py calls it internally.
Apply row-wide borders (e.g. accounting line on a TOTAL row):
After running helper scripts, apply borders to ALL cells in the target row, not just newly added cells. In xl/styles.xml, append a new <border> with the desired style, then append a new <xf> in <cellXfs> that clones each cell's existing <xf> but sets the new borderId. Apply the new style index to every <c> in the row via the s attribute:
xml<!-- In xl/styles.xml, append to <borders>: --> <border> <left/><right/><top style="medium"/><bottom/><diagonal/> </border> <!-- Then append to <cellXfs> an xf clone with the new borderId for each existing style -->
Key rule: When a task says "add a border to row N", iterate over ALL cells A through the last column, not just newly added cells.
Manual XML edit (for anything the helper scripts don't cover):
bashpython3 $SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/ # ... edit XML with `edit` ... python3 $SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
FIX — Repair broken formulas (read references/fix.md first)
This is an EDIT task. Unpack → fix broken <f> nodes → pack. Preserve all original sheets and data.
VALIDATE — Check formulas (read references/validate.md first)
Run formula_check.py for static validation. Use libreoffice_recalc.py for dynamic recalculation when available.
Financial Color Standard
| Cell Role | Font Color | Hex Code |
|---|---|---|
| Hard-coded input / assumption | Blue | 0000FF |
| Formula / computed result | Black | 000000 |
| Cross-sheet reference formula | Green | 00B050 |
Key Rules
- Formula-First for Models, Values for Reports: A model (user will edit inputs) MUST use live formulas for every derived cell. A report (final snapshot) MUST write the already-computed value directly — no formulas, no recalc. Decide by the "Report vs Model" rule above; do not force formulas onto a report.
- CREATE → openpyxl by default (charts/formatting made easy); XML template only when you need byte-level control
- EDIT existing → XML surgery: never openpyxl round-trip on a file you didn't create — it drops VBA/pivots/charts. Use unpack/edit/pack scripts
- Always produce the output file — this is the #1 priority
- Validate before delivery (models only): recalculate (if formulas), then
formula_check.pyexit code 0 = safe. Reports with no formulas skip this. - Recalc is the final step — never overwrite the good copy.
libreoffice_recalc.pynow verifies that formula cells actually got cached<v>values; if it reports empty<v>cells, that is a HARD FAILURE — do not deliver. In a report workflow there are no formulas to recalc, so this never blocks you. If a model's recalc cannot be made to succeed, ship a report-style file (write the computed values) rather than delivering empty formula cells.
Utility Scripts
bashpython3 $SKILL_DIR/scripts/xlsx_reader.py input.xlsx # structure discovery python3 $SKILL_DIR/scripts/formula_check.py file.xlsx --json # formula validation python3 $SKILL_DIR/scripts/formula_check.py file.xlsx --report # standardized report python3 $SKILL_DIR/scripts/xlsx_unpack.py in.xlsx /tmp/work/ # unpack for XML editing python3 $SKILL_DIR/scripts/xlsx_pack.py /tmp/work/ out.xlsx # repack after editing python3 $SKILL_DIR/scripts/xlsx_shift_rows.py /tmp/work/ insert 5 1 # shift rows for insertion python3 $SKILL_DIR/scripts/xlsx_add_column.py /tmp/work/ --col G ... # add column with formulas python3 $SKILL_DIR/scripts/xlsx_insert_row.py /tmp/work/ --at 6 ... # insert row with data


