CSV, but self-describing
Types, units, stats, and SQL sit in the file — not in a README.
The rows stay CSV. pandas, Excel, and awk don't notice.
v0.6 — Draft
date,event,count
2026-01-15,signup,320
2026-01-15,purchase,180
2026-01-16,signup,410
2026-01-16,purchase,340 What's in the file
Tabular exports are everywhere; the note explaining them isn't.
You infer types, rescan for totals, rebuild DDL from memory.
ExCSV is that note — written as # lines any CSV reader already skips.
Still CSV
Metadata is comments. The data section is ordinary rows. Nothing breaks.
Types & units
01720 is a ZIP code. 1050.50 is USD. Write it once, stop guessing.
Stats in the file
Sum, avg, count, min/max on #% lines. The total is a fact, not a second pass over 2M rows.
SQL travels with it
DDL and queries, tagged postgres / mysql / clickhouse. Recreate the table where you're loading.
Unix tools just work
grep, awk, cut on the rows. pandas: comment="#".
Add layers as you go
Header, then types, then stats, then SQL. Stop whenever it's useful. Line-oriented key=value — not a nested blob.
Five ways to ship it
Same language. Different packaging — whether you can touch the CSV, how big it is, one table or several — plus a JSON serialization for when the consumer doesn't speak CSV.
Plain
One file. Schema and rows together. The default.
.excsv + .csvSidecar
Annotate a CSV you can't — or won't — modify.
.excsv.zipZIP
Compress a table. Peek schema and sums without unzipping.
.excsv.pack.zipPack
Wide tables and multi-table snapshots, column by column.
.excsv.jsonJSON
The same document as JSON, for APIs and AI output.
LLM-friendly
Made for AI assistants, too
Paste a table into a chat. The model gets schema, types, units, and totals up front — no guessing the delimiter, no inferring types from five rows, no recomputing the sum.
Types & units
An LLM reads 1050.50 as USD, not a count, and doesn't treat a ZIP code as an integer.
Aggregations as a check
#% rows carry sum, avg, count. The model answers — and sanity-checks its own math — without scanning every row.
Grain, spelled out
#@grain: one row per order says what a row is, instead of forcing the model to reverse-engineer it from column names.
Roles & enums
role= (id / dimension / measure / time) and enum= tell it what to group by, what to sum, and what not to invent.
Agent skill
Teach Claude Code, Codex or Cursor to read ExCSV meta first: answer from #% and
#@grain, load rows in the declared dialect, never sum an id or a balance.
One SKILL.md, no dependencies.
Inspired by
ECSV — Astropy
A CSV can carry a versioned header and per-column schema as # lines and still round-trip through every plain reader. ExCSV keeps the shape, swaps nested YAML for key=value.
Annotated CSV — InfluxDB
One annotation row, one value per data column. ExCSV's #% aggregations (sum, avg, min, …) inherit that layout.
Full credit and a field mapping on the guide.