Herndon is a local-first Python CLI for building Excel workbooks from structured JSON specs.
It is designed for deterministic workbook generation rather than interactive spreadsheet editing. The intended workflow is:
- Create a Herndon project on disk.
- Define workbook and sheet specs as JSON.
- Mutate those specs through the CLI when convenient.
- Validate the workbook before build.
- Render a fresh
.xlsxartifact under.herndon/builds/.
Herndon keeps the workbook source of truth in normal files, not inside Excel binaries.
See SPEC.md for the original product specification. This README explains the current implementation in practical terms.
Herndon currently supports:
- project initialization
- workbook creation
- sheet creation and ordering
- workbook metadata updates
- individual cell writes
- bulk range writes
- formulas as raw Excel formula strings
- merges
- freeze panes
- Excel tables
- chart rendering for
bar,column,line,pie, andscatter - stacked and percent-stacked bar/column charts
- chart legend position, axis titles, labels, and per-series colors
- named themes and named styles
- embedded PNG/JPEG images
- workbook validation
- JSON inspection output for agent use
Current limitations:
- no native histogram chart type
- no round-trip editing of arbitrary existing
.xlsxfiles - no pivot tables, VBA, conditional formatting, or data validation
- formula parsing is intentionally lightweight; formulas are treated mostly as opaque strings
- rendered workbook inspection is not the source of truth; JSON specs are
Herndon is packaged as a Python project with a console entry point.
Editable install:
pip install -e .After that, the CLI is available as:
herndonIf you do not install it, you can still run commands in-process from Python or use the package from the local src/ directory.
The implementation currently uses:
typerfor the CLIpydanticfor schema validationopenpyxlfor workbook renderingPillowfor asset inspection and image generation/loading
Running herndon init creates a project with this layout:
.herndon/
config.json
themes/
builds/
cache/
logs/
workbooks/
assets/
A typical workbook lives under workbooks/<workbook_id>/:
workbooks/
expenses_2026/
workbook.json
sheets/
001-overview.json
002-stacked-mix.json
003-trend.json
Generated output is written to the build path declared in workbook.json, usually something like:
.herndon/builds/expenses_2026/expenses_2026.xlsx
This file defines workbook metadata, theme selection, sheet ordering, and output path.
Example:
{
"version": 1,
"workbook_id": "expenses_2026",
"title": "Expenses 2026",
"theme": "brand",
"sheets": [
"sheets/001-overview.json",
"sheets/002-stacked-mix.json"
],
"build": {
"output": ".herndon/builds/expenses_2026/expenses_2026.xlsx"
}
}Each sheet file contains layout, content, formatting references, and drawable elements.
Main sections:
cellsrangesmergestableschartscolumn_widthsrow_heightsfreeze_rowsfreeze_cols
Themes live under .herndon/themes/ and define named styles used by cells and ranges.
Herndon currently validates that referenced styles exist in the selected theme.
Cells target a single address:
{
"cell": "B8",
"formula": "=SUM(B3:B7)",
"style": "currency_total"
}Supported cell fields:
cellvalueformulastyleimage_pathwh
If image_path is used, w and h are required and treated as inches.
Ranges write a 2D block of values starting at an anchor cell:
{
"anchor": "A2",
"data": [
["Category", "Amount"],
["Rent", 2400],
["Travel", 900]
],
"row_styles": {
"0": "header"
},
"col_styles": {
"1": "currency"
}
}Ranges are rendered before individual cells. If a cell appears in both a range and cells, the explicit cell entry wins.
Excel tables are supported through tables entries:
{
"table_id": "expense_table",
"name": "ExpenseData",
"ref": "A2:B8",
"header_row": true,
"auto_filter": true,
"style": "TableStyleMedium2"
}Supported chart types:
barcolumnlinepiescatter
Supported chart options in the current implementation:
titleanchorwhseriesstackedpercent_stackedshow_legendlegend_positionshow_data_labelsshow_percent_labelsx_axis_titley_axis_titlevalue_format
Series support:
labelvaluescategoriescolor
Example:
{
"chart_id": "stacked_bar",
"chart_type": "bar",
"title": "Spend Mix by Month",
"anchor": "F2",
"w": 8,
"h": 5,
"percent_stacked": true,
"legend_position": "b",
"show_data_labels": true,
"x_axis_title": "Share of Spend",
"y_axis_title": "Month",
"series": [
{
"label": "Rent",
"values": "'Stacked Mix'!$B$2:$B$5",
"categories": "'Stacked Mix'!$A$2:$A$5",
"color": "#2563EB"
}
]
}Notes:
stackedandpercent_stackedare mutually exclusive.- stacked behavior is currently implemented for bar/column charts only.
- series labels and colors are written into the generated chart XML.
openpyxl.load_workbook()does not always round-trip those chart details back into high-level objects cleanly, so XML-level output can be more reliable than object readback for verification.
Charts are embedded by adding objects to the sheet's charts array. They are not stored in workbook.json.
At render time, Herndon:
- writes the sheet data
- builds the chart from the referenced ranges
- anchors the chart at the chart's
anchorcell - sizes it using
wandhin inches
Minimal example inside a sheet file:
{
"sheet_id": "overview",
"title": "Overview",
"freeze_rows": 0,
"freeze_cols": 0,
"zoom": 100,
"column_widths": {},
"row_heights": {},
"cells": [],
"ranges": [
{
"anchor": "A1",
"data": [
["Category", "Amount"],
["Rent", 2400],
["Payroll", 8500],
["Travel", 900]
]
}
],
"merges": [],
"tables": [],
"charts": [
{
"chart_id": "expense_pie",
"chart_type": "pie",
"title": "Spend by Category",
"anchor": "D2",
"w": 7,
"h": 5,
"series": [
{
"label": "Amount",
"values": "'Overview'!$B$2:$B$4",
"categories": "'Overview'!$A$2:$A$4",
"color": "#0F766E"
}
],
"show_legend": true,
"legend_position": "r",
"show_data_labels": true,
"show_percent_labels": true
}
]
}Important details:
anchoris the top-left placement cell for the chart object on the sheet.valuesandcategoriesshould reference cells that will exist after range/cell writes complete.- sheet-qualified ranges should usually use the displayed sheet title, for example
'Overview'!$B$2:$B$4. - charts are rendered after tables and merges.
- if you move the source data, you must update the chart references yourself.
The same pattern applies to line, bar, column, and scatter charts. The only major difference is the chart_type and optional stacking or axis settings.
Images are embedded using cell-anchored cells entries with image_path.
Example:
{
"cell": "H1",
"image_path": "assets/logo.png",
"w": 1.6,
"h": 0.55
}Supported source formats:
- PNG
- JPEG
Relative paths are resolved from the project root.
Within each sheet, Herndon renders in this order:
- column widths and row heights
- range writes
- individual cell writes
- merges
- tables
- charts
- freeze panes
This order is important because it controls overwrite and placement behavior.
Top-level commands:
herndon init
herndon validate
herndon inspect
herndon render
herndon themes
herndon new
herndon workbooks
herndon sheets
herndon assets
Most read-oriented commands support --format json.
herndon init ./my-project
cd ./my-projectherndon new workbook expenses_2026
herndon new sheet workbooks/expenses_2026/workbook.json overviewherndon workbooks set-title workbooks/expenses_2026/workbook.json "Expenses 2026"
herndon workbooks set-theme workbooks/expenses_2026/workbook.json brand
herndon workbooks set-output workbooks/expenses_2026/workbook.json .herndon/builds/expenses_2026/expenses_2026.xlsxSet one cell:
herndon sheets set-cell workbooks/expenses_2026/workbook.json overview A1 --value "2026 Expenditures" --style titleSet a formula:
herndon sheets set-cell workbooks/expenses_2026/workbook.json overview B8 --formula "=SUM(B3:B7)" --style currency_totalSet a block of data:
herndon sheets set-range workbooks/expenses_2026/workbook.json overview A2 \
--data-json '[["Category","Amount"],["Rent",2400],["Payroll",8500],["Travel",900]]' \
--row-styles '{"0":"header"}' \
--col-styles '{"1":"currency"}'herndon sheets add-table workbooks/expenses_2026/workbook.json overview expense_table \
--ref A2:B8 \
--name ExpenseDataPie chart:
herndon sheets add-chart workbooks/expenses_2026/workbook.json overview expense_pie \
--type pie \
--anchor D2 \
--title "Spend by Category" \
--series-json '[{"label":"Amount","values":"'\''Overview'\''!$B$3:$B$7","categories":"'\''Overview'\''!$A$3:$A$7","color":"#0F766E"}]'Line chart:
herndon sheets add-chart workbooks/expenses_2026/workbook.json trend line_trend \
--type line \
--anchor D2 \
--title "Total Spend Trend" \
--legend-position r \
--y-axis-title "Spend" \
--series-json '[{"label":"Total Spend","values":"'\''Trend'\''!$B$2:$B$6","categories":"'\''Trend'\''!$A$2:$A$6","color":"#7C3AED"}]'Percent-stacked bar/column chart:
herndon sheets add-chart workbooks/expenses_2026/workbook.json stacked_mix stacked_bar \
--type bar \
--anchor F2 \
--title "Spend Mix by Month" \
--percent-stacked \
--legend-position b \
--show-data-labels \
--x-axis-title "Share of Spend" \
--y-axis-title "Month" \
--series-json '[{"label":"Rent","values":"'\''Stacked Mix'\''!$B$2:$B$5","categories":"'\''Stacked Mix'\''!$A$2:$A$5","color":"#2563EB"}]'herndon sheets freeze workbooks/expenses_2026/workbook.json overview --rows 2 --cols 0herndon validate workbooks/expenses_2026/workbook.json
herndon validate workbooks/expenses_2026/workbook.json --format jsonInspect emits normalized JSON that is useful for agents and tooling:
herndon inspect workbooks/expenses_2026/workbook.json --format jsonYou can also inspect an individual sheet file.
herndon render workbooks/expenses_2026/workbook.json
herndon render workbooks/expenses_2026/workbook.json --format jsonSuccessful render writes:
- the
.xlsxfile - a build
manifest.json
Implemented sheet commands:
herndon sheets list <workbook.json>herndon sheets add <workbook.json> --sheet <sheet.json> [--after <sheet-id>]herndon sheets remove <workbook.json> <sheet-id> [--delete-files]herndon sheets rename <workbook.json> <sheet-id> <new-sheet-id>herndon sheets duplicate <workbook.json> <sheet-id> <new-sheet-id>herndon sheets move <workbook.json> <sheet-id> --after <sheet-id>herndon sheets set-cell ...herndon sheets set-range ...herndon sheets add-table ...herndon sheets add-chart ...herndon sheets update-chart ...herndon sheets remove-element ...herndon sheets set-merge ...herndon sheets clear-merge ...herndon sheets freeze ...
List project assets:
herndon assets list --format jsonInspect one asset:
herndon assets inspect assets/logo.png --format jsonExample output:
{
"ok": true,
"path": "/abs/path/assets/logo.png",
"width": 180,
"height": 60,
"format": "PNG"
}List installed themes inside the project:
herndon themes --format jsonHerndon currently validates:
- workbook and sheet schema shape
- duplicate sheet IDs
- duplicate table names across the workbook
- missing theme references
- invalid cell and range addresses
- invalid row/column dimension references
- formulas that do not start with
= - unresolved sheet references in formulas as warnings
- chart references to unknown sheets as warnings
- merge overlap with table ranges
- unknown named styles
- missing image assets
Validation returns non-zero if any errors are present.
Read-oriented commands support --format json and are intended to be machine-friendly.
Typical validation output:
{
"ok": false,
"issues": [
{
"severity": "error",
"code": "unknown_style",
"path": "/abs/path/workbooks/demo/sheets/001-summary.json",
"field": "/cells/0/style",
"message": "Style 'header2' is not defined in theme 'brand'"
}
]
}CLI error categories used by the implementation:
usage_errorschema_errorvalidation_errorasset_errorrender_errorio_error
The repository includes a practical demo project at:
Key files:
The demo workbook currently exercises:
- a pie chart
- a percent-stacked chart
- a line chart
- an Excel table
- formulas
- named styles
- an embedded image
The project includes CLI and render tests under tests/.
Run them with:
pytest -qCurrent coverage includes:
- project initialization
- workbook/sheet creation
- workbook metadata updates
- inspection golden output
- validation behavior
- table rendering
- pie chart rendering
- stacked/percent-stacked and line chart rendering
- asset inspection
- embedded image rendering
Main source files:
src/herndon/cli.pysrc/herndon/models.pysrc/herndon/project.pysrc/herndon/validation.pysrc/herndon/inspection.pysrc/herndon/renderer.pysrc/herndon/errors.py
Rough responsibility split:
models.py: schema objectsproject.py: loading workbooks, sheets, and themesvalidation.py: workbook/sheet validationinspection.py: normalized inspection outputrenderer.py:.xlsxgenerationcli.py: user-facing command surface
The .xlsx output is standard and can be opened in Excel-compatible tools, but GUI behavior depends on the installed office suite.
For this repository, direct XML inspection of the rendered workbook is sometimes a better compatibility check than reading chart objects back through openpyxl, because some chart features do not round-trip perfectly through its read API.