Agent-first CLI for Excel workbooks
Project description
xl
Agent-first CLI for Excel workbooks (.xlsx / .xlsm)
A transactional spreadsheet execution layer that lets AI agents and humans inspect, plan, validate, and apply workbook changes deterministically — like terraform for Excel.
xl wb inspect -f budget.xlsx # discover structure
xl plan add-column -f budget.xlsx \ # generate a plan (non-mutating)
-t Sales -n Margin \
--formula "=[@Revenue]-[@Cost]" \
--out plan.json
xl validate plan -f budget.xlsx \ # check before applying
--plan plan.json
xl apply -f budget.xlsx \ # preview changes
--plan plan.json --dry-run
xl apply -f budget.xlsx \ # apply with safety backup
--plan plan.json --backup
xl verify assert -f budget.xlsx \ # confirm expected state
--assertions '[{"type":"table.column_exists","table":"Sales","column":"Margin"}]'
Every command returns a structured JSON envelope:
{
"ok": true,
"command": "table.ls",
"result": [...],
"errors": [],
"warnings": [],
"metrics": { "duration_ms": 42 }
}
No Excel installation required — works entirely with openpyxl.
Features
- Inspect — sheets, tables, named ranges, fingerprints, lock status
- Read — cell values, range statistics (min/max/mean/sum/count/stddev), formula search by regex
- Query — SQL over table data via DuckDB (
SELECT Region, SUM(Sales) FROM Sales GROUP BY Region) - Mutate — add/delete columns, append rows, set cells/formulas, number formats, column widths, freeze panes
- Patch plans — generate JSON plans, compose multi-step plans, validate before applying
- Apply — execute plans with
--dry-runpreview and--backupsafety copy - Verify — post-apply assertions (
table.column_exists,cell.value_equals,row_count.gte, ...) - Diff — cell-level comparison between two workbook files
- Lint — detect volatile functions, broken references, formula anti-patterns
- Workflows — multi-step YAML pipelines with
xl run - Server mode — stdio JSON server for agent tool integration (MCP/ACP)
Safety rails
All mutating commands include built-in protections:
| Feature | Description |
|---|---|
--dry-run |
Preview every change without writing to disk |
--backup |
Timestamped .bak copy before any write |
| Fingerprint conflict detection | Plans record the file's xxhash; apply rejects if the workbook changed since the plan was created |
| Exclusive file locking | Sidecar .xl.lock prevents concurrent mutations; --wait-lock N retries for N seconds |
| Formula protection | Refuses to overwrite existing formulas unless --force-overwrite-formulas is set |
| Policy files | Optional xl-policy.yaml for protected sheets, ranges, mutation thresholds, and command restrictions |
Installation
Requires Python 3.12+ and uv.
From PyPI (recommended)
uv tool install xl-agent-cli
# Verify
xl version
xl --help
Local development install
git clone https://github.com/ThomasRohde/xl-cli.git
cd xl-cli
uv sync
# Verify
uv run xl version
uv run xl --help
Global install from source
git clone https://github.com/ThomasRohde/xl-cli.git
cd xl-cli
uv tool install --from . xl-agent-cli
If xl is not found, open a new terminal so your PATH refreshes.
Quick start
1. Inspect a workbook
xl wb inspect -f data.xlsx
Returns sheets, tables (with columns and row counts), named ranges, and a fingerprint hash.
2. List tables
xl table ls -f data.xlsx
3. Query with SQL
xl query -f data.xlsx \
--sql "SELECT Region, SUM(Revenue) as Total FROM Sales GROUP BY Region ORDER BY Total DESC"
4. Add a calculated column (safe workflow)
# Generate a plan (does NOT modify the workbook)
xl plan add-column -f data.xlsx -t Sales -n GrossMarginPct \
--formula "=[@GrossMargin]/[@Revenue]" \
--out plan.json
# Validate the plan
xl validate plan -f data.xlsx --plan plan.json
# Preview what would change
xl apply -f data.xlsx --plan plan.json --dry-run
# Apply for real with backup
xl apply -f data.xlsx --plan plan.json --backup
# Verify the result
xl verify assert -f data.xlsx \
--assertions '[{"type":"table.column_exists","table":"Sales","column":"GrossMarginPct"}]'
5. Format a report
xl format number -f report.xlsx --ref "Sales[Revenue]" --style currency --decimals 2
xl format width -f report.xlsx --sheet Sheet1 --columns A,B,C,D --width 15
xl format freeze -f report.xlsx --sheet Sheet1 --ref B2
6. Compare before and after
xl diff compare --file-a original.xlsx --file-b modified.xlsx
Command reference
| Group | Commands | Description |
|---|---|---|
xl wb |
inspect, create, lock-status |
Workbook metadata, creation, fingerprint, lock check |
xl sheet |
ls, create, delete, rename |
Sheet lifecycle and management |
xl table |
create, ls, add-column, append-rows, delete, delete-column |
Table operations |
xl cell |
get, set |
Read/write individual cells |
xl range |
stat, clear |
Range statistics and clearing |
xl formula |
set, lint, find |
Set formulas, lint for issues, search by regex |
xl format |
number, width, freeze |
Number formats, column widths, freeze panes |
xl query |
(top-level) | SQL queries over table data via DuckDB |
xl plan |
show, add-column, create-table, set-cells, format, compose, delete-sheet, rename-sheet, delete-table, delete-column |
Generate and compose patch plans |
xl validate |
workbook, plan, refs, workflow |
Validate health, plans, references, workflows |
xl apply |
(top-level) | Apply patch plans with --dry-run and --backup |
xl verify |
assert |
Post-apply assertions |
xl diff |
compare |
Cell-level workbook comparison |
xl run |
(top-level) | Execute multi-step YAML workflows |
xl serve |
(top-level) | stdio server for agent tool integration |
xl guide |
(top-level) | Machine-readable JSON orientation guide |
Use xl <group> --help or xl <group> <command> --help for detailed usage.
Reference syntax
Commands that target cells, ranges, or table columns use ref syntax:
| Pattern | Example | Usage |
|---|---|---|
SheetName!Cell |
Sheet1!B2 |
Single cell |
SheetName!Start:End |
Sheet1!A1:D10 |
Cell range |
TableName[ColumnName] |
Sales[Revenue] |
Table column (formula/format) |
=[@ColumnName] |
=[@Revenue]-[@Cost] |
Structured ref inside table formulas |
Exit codes
| Code | Meaning |
|---|---|
0 |
Success |
10 |
Validation error (bad input, schema mismatch, invalid plan) |
20 |
Protection error (protected range or sheet) |
30 |
Formula error (overwrite blocked, parse failure) |
40 |
Conflict (fingerprint mismatch — workbook changed) |
50 |
IO error (file not found, locked, permission denied) |
60 |
Recalculation error |
70 |
Unsupported operation |
90 |
Internal error |
Agent integration
xl is designed to be driven by AI coding agents. Every command returns machine-parseable JSON, uses deterministic exit codes, and provides progressive discovery:
xl guide # full structured JSON orientation (start here)
xl --help # command group overview
xl table --help # group detail with examples
xl table add-column --help # command detail with usage examples
Token-optimized help (TOON)
When an LLM drives the CLI, verbose Rich-formatted --help output wastes tokens. Set LLM=true to switch all help to TOON (Token-Oriented Object Notation) — a compact key:value format that cuts token usage by ~75%.
# Enable TOON for the session
export LLM=true # bash/zsh
$env:LLM = "true" # PowerShell
xl --help
name: xl
description: Agent-first CLI for reading transforming and validating Excel workbooks
options[2]:
flag,type,required,default,help
--version/-V,flag,false,false,Print version and exit.
--human,flag,false,false,Force human-readable help (overrides LLM=true).
groups[11]:
name,description
cell,Read and write individual cell values.
table,Table operations — list add columns append rows.
...
The --human flag overrides LLM=true for a single invocation:
xl --human --help # Rich output even with LLM=true set
| Condition | Help format |
|---|---|
| Default (no env var) | Rich/Markdown (human-readable) |
LLM=true |
TOON (compact, token-optimized) |
LLM=true + --human |
Rich/Markdown (override) |
stdio server mode
For tool-use integrations (MCP, ACP, or custom):
xl serve --stdio
Reads JSON commands from stdin, writes JSON responses to stdout.
Development
# Install with dev dependencies
uv sync
# Run all tests
uv run pytest tests/ -v
# Run a specific test file
uv run pytest tests/test_cli.py -v
# Run the CLI directly
uv run xl --help
Tech stack
| Component | Library |
|---|---|
| Excel engine | openpyxl |
| CLI framework | Typer |
| Data models | Pydantic v2 |
| JSON serialization | orjson |
| SQL queries | DuckDB |
| Terminal output | Rich |
| YAML parsing | PyYAML |
| Structured logging | structlog |
| File locking | portalocker |
| Fingerprinting | xxhash |
| Build | hatchling |
Project structure
src/xl/
├── cli.py # Typer CLI app, all command definitions
├── contracts/ # Pydantic models (ResponseEnvelope, PatchPlan, WorkflowSpec)
├── engine/ # WorkbookContext, response dispatcher, verify, workflow runner
├── adapters/ # openpyxl engine, DuckDB queries
├── validation/ # Plan/workbook validators, policy engine
├── io/ # Fingerprint, backup, atomic write, file locking
├── observe/ # Timer and event utilities
├── diff/ # Workbook comparison logic
├── help/ # TOON help output for LLM consumers
└── server/ # stdio server mode
tests/ # Unit, integration, golden, property, performance tests
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xl_agent_cli-1.6.0.tar.gz.
File metadata
- Download URL: xl_agent_cli-1.6.0.tar.gz
- Upload date:
- Size: 177.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bdc0fe0239da01cfb167d1374804b87b17294feae926ec843be0c26e163876fa
|
|
| MD5 |
e17d495f93fd76da68fe87fdca1261c6
|
|
| BLAKE2b-256 |
f5d677ff4f039fb7a6f39d3f238659f068d9eed3b31e2ae549a3b7f9e0fdc644
|
Provenance
The following attestation bundles were made for xl_agent_cli-1.6.0.tar.gz:
Publisher:
pypi.yml on ThomasRohde/xl-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xl_agent_cli-1.6.0.tar.gz -
Subject digest:
bdc0fe0239da01cfb167d1374804b87b17294feae926ec843be0c26e163876fa - Sigstore transparency entry: 995160711
- Sigstore integration time:
-
Permalink:
ThomasRohde/xl-cli@fa43d9cc746705cc38195ef2262f7c1e41e79fa8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ThomasRohde
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@fa43d9cc746705cc38195ef2262f7c1e41e79fa8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file xl_agent_cli-1.6.0-py3-none-any.whl.
File metadata
- Download URL: xl_agent_cli-1.6.0-py3-none-any.whl
- Upload date:
- Size: 73.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd4987efc7b2a901b3002161a007c898cb5a06de6fd7141db555f0bb6873987f
|
|
| MD5 |
f609573795409842c8af32f61ebc9244
|
|
| BLAKE2b-256 |
309734666e5ca231704476b7fe1458c7386735ed0abd78f68f3cb3c7da697f0a
|
Provenance
The following attestation bundles were made for xl_agent_cli-1.6.0-py3-none-any.whl:
Publisher:
pypi.yml on ThomasRohde/xl-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xl_agent_cli-1.6.0-py3-none-any.whl -
Subject digest:
dd4987efc7b2a901b3002161a007c898cb5a06de6fd7141db555f0bb6873987f - Sigstore transparency entry: 995160716
- Sigstore integration time:
-
Permalink:
ThomasRohde/xl-cli@fa43d9cc746705cc38195ef2262f7c1e41e79fa8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ThomasRohde
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@fa43d9cc746705cc38195ef2262f7c1e41e79fa8 -
Trigger Event:
push
-
Statement type: