Your first sheet
This walkthrough builds a tiny customers sheet from scratch. Nothing here needs an API key.
Already done the Quickstart? This page
is the deeper version: instead of accepting folio init’s placeholder
contract, you author the real customer schema by hand and add a
derivation. The two pages don’t conflict — they just sit at different
points on the convenience/understanding curve.
1. Create the directory
mkdir -p customers/derivations customers/scriptscd customersPrefer to start from a runnable scaffold? folio init --name customers
writes contract.yaml / records.jsonl / README.md / a starter
skill in one step. The rest of this page then becomes “swap the stub
contract for the customer schema below.”
2. Write the contract
contract.yaml:
apiVersion: v3.0.0kind: DataContractid: my-customersname: customersversion: 1.0.0schema: - name: items physicalType: jsonl properties: - name: id logicalType: string primaryKey: true required: true - name: company_name logicalType: string required: true x-editable-by: ["agent:human"] - name: country logicalType: string required: true x-editable-by: ["agent:human"] - name: country_code logicalType: string x-derived: true x-inputs: [country]A single primary key (id), two human-editable fields (company_name,
country), and one derived field (country_code).
3. Add some records
records.jsonl — one JSON object per line, no trailing comma:
{"id": "cust_001", "company_name": "Acme Manufacturing", "country": "Japan"}{"id": "cust_002", "company_name": "DataFlow", "country": "United States"}{"id": "cust_003", "company_name": "NordicSoft", "country": "Sweden"}4. Validate the spec
$ folio validate .contract.yaml is valid: my-customers v1.0.0 (4 fields)records.jsonl is readable (3 records)If there’s a typo (a missing required field, a duplicate primary key, a logical
type that doesn’t exist), folio validate exits non-zero with a clear
message. Run it any time you edit contract.yaml.
5. Add a derivation python
Tell Folio how to derive country_code. We’ll use the python kind
because it’s offline and trivial.
derivations/country_code.yaml:
targets: [country_code]inputs: [country]kind: pythonscript: country_to_codescripts/country_to_code.py:
"""Map a country name to ISO-3166-1 alpha-2 (offline)."""import json, sys
ISO_BY_NAME = { "Japan": "JP", "United States": "US", "Sweden": "SE",}
inputs = json.loads(sys.argv[2])print(ISO_BY_NAME.get(inputs.get("country", ""), "??"))6. Materialize
$ folio materialize . --actor agent:demo{"materialized": 3, "skipped": 0, "failures": [], "total_cost": 0.0}folio ran the script three times (once per record), wrote the results back
to records.jsonl, and appended three lines to provenance.jsonl.
$ folio query . "SELECT id, country, country_code FROM records ORDER BY id"[{"id":"cust_001","country":"Japan","country_code":"JP"}, {"id":"cust_002","country":"United States","country_code":"US"}, {"id":"cust_003","country":"Sweden","country_code":"SE"}]7. Run materialize again — cache hits
$ folio materialize . --actor agent:demo{"materialized": 0, "skipped": 3, "failures": [], "total_cost": 0.0}Three skipped: every record’s input hash is unchanged, so Folio reads the
cached output instead of rerunning the script. The cache lives outside the
sheet (<user-cache>/folio/my-customers/cache/) so the sheet stays portable.
8. Inspect provenance
$ folio provenance . cust_001 country_code{"record_id":"cust_001","field":"country_code","source":"python", "actor":"agent:demo","at":"2026-05-10T10:16:35Z", "input_hash":"sha256:ce82..."}--history returns the full append-only chain.
9. Edit through the Viewer
folio serve . --port 3000 --actor agent:human# → http://127.0.0.1:3000/In the Viewer:
- The Records tab shows all three rows. The
country_codecolumn has a python dot — that’s its provenance. company_nameandcountryare inline-editable foragent:human.- Edit
cust_002.countrytoGermanyand tab out. The Viewer writes ahumanprovenance entry. Then click Materialize all on the dashboard — Folio sees the input changed, invalidates the cache forcust_002, re-runs the script, and writescountry_code: "??"(Germany isn’t in our tiny lookup; add it to the script and re-materialize to fix it).
What you have
A complete, portable Folio sheet. tar czf customers.tgz customers/ and ship
it; the recipient runs folio validate customers/ and gets the same answers.
Where to go next
- Materialize lifecycle — what the cache really keys on, when it invalidates, and how to force a rerun.
- Editing and provenance — what happens when humans and agents touch the same field.
- Derivations overview — the other five kinds (ai / import / sql / http / cross_sheet).