Customer master enrichment
This guide walks through Use Case 2.1 from the design overview
(§2.1).
The shipped sheets are
examples/customers/
and examples/customer-revenue/.
The shape
A customers sheet where humans (or a sales agent) populate the basic
columns, and Folio fills derived fields automatically.
contract.yaml columns: id string PK company_name string human-editable country string human-editable industry_name string human-editable country_code string derived (python) current_revenue_usd number derived (cross_sheet ← customer-revenue)The data flow:
┌──────────────┐ upsert ┌──────────────────────────┐ │ sales agent │──────────────▶ │ customers/records.jsonl │ └──────────────┘ └──────────────────────────┘ │ │ folio materialize ▼ ┌─────────────────────┐ ┌─────────────────────────┐ │ python country code │ │ cross_sheet to revenue │ │ scripts/... │ │ ../customer-revenue │ └─────────────────────┘ └─────────────────────────┘ │ ▼ ┌──────────────────────────────┐ │ humans review weekly via │ │ folio serve (Viewer) │ └──────────────────────────────┘Walking through the sheet
git clone --depth 1 https://github.com/nyuta01/folio.gitcd folio
folio validate examples/customersfolio validate examples/customer-revenueBoth contracts validate. Now materialize:
folio materialize examples/customers --actor agent:demo# {"materialized": 12, "skipped": 0, "failures": [], "total_cost": 0.0}Twelve cells: 7 records × 2 derived columns = 14, but cust_006 and
cust_007 aren’t in customer-revenue so their current_revenue_usd
stays null (cross_sheet’s “no match” behaviour — not an error).
Open the Viewer:
folio serve examples/customers --port 3000 --actor agent:humanVisit http://127.0.0.1:3000/:
- Records tab — the 7 customers, with type chips (
string,number), provenance dots (python oncountry_code, cross sheet oncurrent_revenue_usd), and inline<input>editors oncompany_name/country/industry_name. - Dashboard tab — two cards (one per derived target), each with the per-source count and the last run timestamp.
- History tab — click a non-editable cell on Records to inspect its append-only chain.
A typical week
-
Monday — sales agent imports. New leads arrive as a CSV. The agent
upserts them:Terminal window folio upsert examples/customers --actor agent:sales \--file new-leads.jsonl -
Monday afternoon — derivations fill. A nightly cron triggers:
Terminal window folio materialize examples/customers --actor agent:nightlycountry_coderuns for the new rows;current_revenue_usdjoins against the latestcustomer-revenue/records.jsonl. Existing rows cache-hit. -
Tuesday — finance updates revenue. Finance updates
customer-revenue/records.jsonl. The next materialize sees the foreign sheet’s hash flipped and re-joins every customer (per thecross_sheetcache rules). -
Friday — human review. A reviewer opens the Viewer, fixes a few
country_codevalues (the deterministic lookup misses some territories), and edits inline. Each fix appends ahumanprovenance line. The next materialize does not overwrite those edits unless--forceis passed.
What’s not in this guide
- AI-driven industry classification. This guide stays offline. To add
it, define an
aiderivation againstindustry_tagwith a prompt that classifiescompany_name+countryinto a controlled vocabulary. SetANTHROPIC_API_KEYand re-runfolio materialize. - Quality-control checks. A natural extension is a
pythonderivation that flags rows wherecountrydoesn’t match a known list, surfacing them for review.
See also
- examples/customers/README.md
cross_sheetderivation — semantics and cache rules for the 1:1 sidecar pattern.- Editing and provenance — the human-review loop in detail.