office-xlsx — native XLSX with openpyxl
You build editable .xlsx workbooks with Python openpyxl: real formulas the recipient can recalc, explicit number formats and fonts, and native charts. You can also read and parse existing workbooks. No LibreOffice.
Runtime contract
uvmust be onPATH. All Python runs throughuv run; the runtime injects the package-mirror environment variables souvresolvesopenpyxlfrom the internal mirror. You do not configure the mirror.- If
uvis missing, stop and report exactly:office-xlsx requires 'uv' on PATH, but 'uv --version' failed. This is an environment problem — uv should be provisioned by the PawWork runtime. Do not fall back to system pip; report the missing uv instead.Do not attemptpip installor any other fallback. - The skill directory ships read-only inside the app bundle. Never write into it. Work in a fresh directory.
SKILL_DIR below means the skill's base directory as a plain filesystem path — copy the line labeled "Base directory as a plain filesystem path" from the skill-load output. Do not use the file:// URL form; if only a file:// URL is available, strip the file:// prefix first.
SKILL_DIR="<plain filesystem path from the skill-load output, no file:// prefix>"
uv --version || { echo "office-xlsx requires 'uv' on PATH (see Runtime contract)"; exit 1; }
mkdir -p work && cp "$SKILL_DIR/pyproject.toml" work/pyproject.toml # pins openpyxl==3.1.5
Run every uv run command from work/.
Authoring rules (hard floor — do not relax)
- Real formulas, not baked-in numbers. When a cell is a computed result (totals, ratios, growth, margins), write the Excel formula string (
ws["B10"] = "=SUM(B2:B9)"), never the pre-computed value. The recipient must be able to change an input and see downstream cells recalc. The delivery gate fails a data model with zero formula cells when--require-formulais set. - Formula injection: only YOUR computed cells may be formulas. Any value that comes from user-supplied data (CSV imports, pasted text, names, notes) and starts with
=,+,-, or@must be stored as text, never as a formula — e.g. a user field=HYPERLINK("http://evil","x")would otherwise become a live formula. Force text withcell.value = raw; cell.data_type = "s"; cell.number_format = "@". When you genuinely need a link, use thecell.hyperlinkattribute, never theHYPERLINK()formula. The gate FAILs on formulas callingHYPERLINK/WEBSERVICE/IMPORT*/ DDE-style patterns. - Explicit number formats. Set
cell.number_formatfor every money / percent / date column (e.g."#,##0.00","0.0%","yyyy-mm-dd"). Do not rely on the defaultGeneralformat for financial output. - Explicit fonts and styles. Set header rows with an explicit
Font(bold=True, size=...)and, where it aids reading,PatternFill/Alignment/Border. Do not leave headers visually identical to body rows. - Freeze panes and widths. Freeze the header row (
ws.freeze_panes = "A2") and set sensiblecolumn_dimensions[col].widthso columns are not clipped. - Charts are native objects. When the task calls for a chart, build it with
openpyxl.chart(BarChart,LineChart,PieChart, ...) bound to aReferenceover the data range — a real chart object, not a pasted image. - One concern per sheet. Split inputs / model / outputs across named sheets rather than cramming everything into
Sheet1.
Minimal shape:
from openpyxl import Workbook
from openpyxl.styles import Font
wb = Workbook()
ws = wb.active
ws.title = "Model"
ws["A1"] = "Quarter"; ws["B1"] = "Revenue"
ws["A1"].font = ws["B1"].font = Font(bold=True, size=12)
ws["A2"] = "Q1"; ws["B2"] = 100
ws["A3"] = "Q2"; ws["B3"] = 140
ws["A4"] = "Total"; ws["B4"] = "=SUM(B2:B3)" # real formula
ws["B2"].number_format = ws["B3"].number_format = ws["B4"].number_format = "#,##0"
ws.freeze_panes = "A2"
wb.save("out.xlsx")
Reading / parsing
load_workbook(path) reads formulas; load_workbook(path, data_only=True) reads the last cached computed values. Use data_only=True when you need results, data_only=False when you need to inspect or preserve formulas. To edit an existing workbook, load_workbook(path), change cells (respecting the formula-injection rule above for any user-supplied values), then wb.save('out.xlsx').
Gate — structure check (before delivering)
Run the structure gate and fix every reported gap by rebuilding from your generator — never hand-edit the zip — until it prints OK:
uv run python "$SKILL_DIR/scripts/check_xlsx.py" out.xlsx --min-sheets 1 [--require-formula] [--require-chart]
Pass --require-formula for any calculation / model task and --require-chart when the task calls for a chart. The gate confirms the workbook opens, has populated cells, flags injection-style formulas, and (when required) contains real formula cells and chart parts. Charts are counted from the package's xl/charts/chart*.xml parts — a stable OOXML signal — so the gate does not depend on openpyxl's chart read-back.
The workbook is not done until this gate prints OK. The machine check decides completion, not your own judgement.