Add a cross_sheet derivation to a Folio sheet
Author a derivations/<target>.yaml of kind: cross_sheet that joins
this sheet 1:1 with a sibling sheet on the calling sheet's primary
key, and verify with one materialize.
When this skill applies
- Two Folio sheets exist side by side, share an identifier 1:1, and
the user wants one of them to pull a field from the other.
- The classic shape: a
customers/ sheet next to a customer-revenue/
sheet, both keyed by id. customers wants current_revenue_usd
copied in from customer-revenue.
- The user wants Folio's contract on the foreign sheet enforced (so
bad foreign data fails validation rather than poisoning the join).
This skill does not apply when:
- The foreign source is not a Folio sheet (CSV, JSON file, HTTP
endpoint) — use
kind: import instead.
- The join key is not the calling sheet's primary key — the
built-in
cross_sheet only matches the foreign key_field
against the calling sheet's PK. Use import or a python
derivation that does the lookup explicitly.
- The relationship is 1:N or M:N —
cross_sheet writes one cell per
calling-sheet record. Aggregations need kind: sql or kind: python.
Prerequisites
- The calling sheet exists, validates, and has the target field
declared
x-derived: true in contract.yaml.
- The foreign sheet exists at a path resolvable from the calling
sheet (typically
../<sibling>), validates, and contains the
field you want to copy.
- Both sheets agree on the PK value space (identical strings, same
case, no leading/trailing whitespace).
Procedure
Confirm the sidecar layout. Folio resolves source_sheet
relative to the calling sheet's root. The conventional layout:
customers/ customer-revenue/
├── contract.yaml ──→ ├── contract.yaml
├── records.jsonl ├── records.jsonl
│ (id, revenue_usd, ...)
└── derivations/
└── revenue.yaml ◀── reads from ../customer-revenue
Add the target field to contract.yaml of the calling sheet,
marked derived:
- name: current_revenue_usd
logicalType: number
x-derived: true
x-inputs: [] # join is by PK, no other inputs
Write derivations/<target>.yaml in the calling sheet.
Single-target skeleton:
# customers/derivations/revenue.yaml
targets: [current_revenue_usd]
inputs: [] # join is by PK, no other inputs
kind: cross_sheet
source_sheet: ../customer-revenue # path relative to this sheet
key_field: id # field on the foreign sheet
value_field: revenue_usd # field whose value to copy
Multi-target (all updated atomically, share one input_hash):
targets: [current_revenue_usd, contract_value_usd, finance_as_of]
inputs: []
kind: cross_sheet
source_sheet: ../customer-revenue
key_field: id
value_fields: # mutually exclusive with value_field
current_revenue_usd: revenue_usd
contract_value_usd: contract_value_usd
finance_as_of: as_of
Validate both sheets, then materialize the calling one.
folio validate runs against the foreign sheet too — broken foreign
data shows up here, not silently in the join.
folio validate ./customers
folio validate ./customer-revenue
folio materialize ./customers current_revenue_usd --actor agent:demo
The derivation target is a positional argument to folio materialize; omit it to materialize every derivation.
The §10.6 envelope:
{"materialized": 2, "skipped": 0, "failures": [], "total_cost": 0.0}
Spot-check a row.
folio query ./customers \
"SELECT id, current_revenue_usd FROM records ORDER BY id LIMIT 5"
Verify
folio validate <sheet>
folio validate <foreign_sheet>
folio materialize <sheet> <field> --actor agent:demo
All three should exit 0. The materialize envelope's failures should
be []. Records with no foreign match keep the field null and are
not counted as failures (see below).
Cache behaviour
input_hash for a cross_sheet cell includes:
- the canonical JSON of every input value (often empty),
- the calling sheet's primary key value,
- the SHA-256 of the foreign sheet's
records.jsonl,
- the SHA-256 of the derivation file.
That third item is load-bearing: edit the foreign records.jsonl,
every calling-side row's hash changes, and the next materialize
re-joins everything. Right behaviour when you don't know which
foreign rows changed.
If the foreign sheet is huge and changes constantly, prefer a
snapshot via kind: import instead.
"No match" semantics
If the foreign sheet has no row whose <key_field> equals the
calling sheet's PK, Folio writes nothing for that cell, the value
stays null, and nothing is reported on the envelope. Missing
foreign rows are an expected case for cross_sheet.
If you want a failure in that case, layer a kind: python
derivation downstream that asserts the field is non-null after
cross_sheet runs.
Common mistakes (don't make them)
- Joining by a non-PK field.
cross_sheet matches the foreign
key_field against the calling sheet's PK, not against an
arbitrary calling-side input. If you need lookup by an input,
reach for import or python.
- Both
value_field and value_fields set. They are mutually
exclusive. Folio rejects.
- Wrong relative path on
source_sheet. Paths are relative to
the calling sheet's root, not to derivations/. Most sidecar
setups use ../<other_sheet>.
- PK mismatch. Trailing whitespace, case differences, or
numeric-vs-string discrepancies will silently produce no match
(see "No match" above). Normalize on the source side.
- Foreign records edited but the calling sheet not re-materialized.
The cache flips correctly on the next run; what's wrong is letting
agents read the stale
current_revenue_usd in between. Re-run
materialize after foreign updates, or schedule it.
1---2name: add-derivation-cross-sheet3description: Wire up a Folio `kind: cross_sheet` derivation — pull a field from a sibling Folio sheet that shares the same primary key (the 1:1 sidecar pattern). Invoke when the user wants to "join two sheets", "pull revenue from the finance sheet into customers", or otherwise copy values keyed by PK from one sheet to another.4---56# Add a `cross_sheet` derivation to a Folio sheet78Author a `derivations/<target>.yaml` of `kind: cross_sheet` that joins9this sheet 1:1 with a sibling sheet on **the calling sheet's primary10key**, and verify with one materialize.1112## When this skill applies1314- Two Folio sheets exist side by side, share an identifier 1:1, and15 the user wants one of them to pull a field from the other.16- The classic shape: a `customers/` sheet next to a `customer-revenue/`17 sheet, both keyed by `id`. `customers` wants `current_revenue_usd`18 copied in from `customer-revenue`.19- The user wants Folio's contract on the foreign sheet enforced (so20 bad foreign data fails validation rather than poisoning the join).2122This skill does **not** apply when:2324- The foreign source is **not** a Folio sheet (CSV, JSON file, HTTP25 endpoint) — use `kind: import` instead.26- The join key is **not** the calling sheet's primary key — the27 built-in `cross_sheet` only matches the foreign `key_field`28 against the calling sheet's PK. Use `import` or a `python`29 derivation that does the lookup explicitly.30- The relationship is 1:N or M:N — `cross_sheet` writes one cell per31 calling-sheet record. Aggregations need `kind: sql` or `kind: python`.3233## Prerequisites3435- The calling sheet exists, validates, and has the target field36 declared `x-derived: true` in `contract.yaml`.37- The foreign sheet exists at a path resolvable from the calling38 sheet (typically `../<sibling>`), validates, and contains the39 field you want to copy.40- Both sheets agree on the PK value space (identical strings, same41 case, no leading/trailing whitespace).4243## Procedure44451. **Confirm the sidecar layout.** Folio resolves `source_sheet`46 relative to the calling sheet's root. The conventional layout:4748 ```text49 customers/ customer-revenue/50 ├── contract.yaml ──→ ├── contract.yaml51 ├── records.jsonl ├── records.jsonl52 │ (id, revenue_usd, ...)53 └── derivations/54 └── revenue.yaml ◀── reads from ../customer-revenue55 ```56572. **Add the target field to `contract.yaml`** of the calling sheet,58 marked derived:5960 ```yaml61 - name: current_revenue_usd62 logicalType: number63 x-derived: true64 x-inputs: [] # join is by PK, no other inputs65 ```66673. **Write `derivations/<target>.yaml`** in the calling sheet.68 Single-target skeleton:6970 ```yaml71 # customers/derivations/revenue.yaml72 targets: [current_revenue_usd]73 inputs: [] # join is by PK, no other inputs74 kind: cross_sheet75 source_sheet: ../customer-revenue # path relative to this sheet76 key_field: id # field on the foreign sheet77 value_field: revenue_usd # field whose value to copy78 ```7980 Multi-target (all updated atomically, share one `input_hash`):8182 ```yaml83 targets: [current_revenue_usd, contract_value_usd, finance_as_of]84 inputs: []85 kind: cross_sheet86 source_sheet: ../customer-revenue87 key_field: id88 value_fields: # mutually exclusive with value_field89 current_revenue_usd: revenue_usd90 contract_value_usd: contract_value_usd91 finance_as_of: as_of92 ```93944. **Validate both sheets, then materialize the calling one.**95 `folio validate` runs against the foreign sheet too — broken foreign96 data shows up here, not silently in the join.9798 ```bash99 folio validate ./customers100 folio validate ./customer-revenue101 folio materialize ./customers current_revenue_usd --actor agent:demo102 ```103104 The derivation target is a positional argument to `folio105 materialize`; omit it to materialize every derivation.106107 The §10.6 envelope:108109 ```json110 {"materialized": 2, "skipped": 0, "failures": [], "total_cost": 0.0}111 ```1121135. **Spot-check a row.**114115 ```bash116 folio query ./customers \117 "SELECT id, current_revenue_usd FROM records ORDER BY id LIMIT 5"118 ```119120## Verify121122```bash123folio validate <sheet>124folio validate <foreign_sheet>125folio materialize <sheet> <field> --actor agent:demo126```127128All three should exit 0. The materialize envelope's `failures` should129be `[]`. Records with no foreign match keep the field `null` and are130**not** counted as failures (see below).131132## Cache behaviour133134`input_hash` for a `cross_sheet` cell includes:135136- the canonical JSON of every input value (often empty),137- the calling sheet's primary key value,138- the SHA-256 of the foreign sheet's `records.jsonl`,139- the SHA-256 of the derivation file.140141That third item is load-bearing: edit the foreign `records.jsonl`,142*every* calling-side row's hash changes, and the next materialize143re-joins everything. Right behaviour when you don't know which144foreign rows changed.145146If the foreign sheet is huge and changes constantly, prefer a147snapshot via `kind: import` instead.148149## "No match" semantics150151If the foreign sheet has no row whose `<key_field>` equals the152calling sheet's PK, Folio writes nothing for that cell, the value153stays `null`, and **nothing is reported on the envelope**. Missing154foreign rows are an expected case for `cross_sheet`.155156If you want a *failure* in that case, layer a `kind: python`157derivation downstream that asserts the field is non-null after158`cross_sheet` runs.159160## Common mistakes (don't make them)161162- **Joining by a non-PK field.** `cross_sheet` matches the foreign163 `key_field` against the **calling sheet's PK**, not against an164 arbitrary calling-side input. If you need lookup by an input,165 reach for `import` or `python`.166- **Both `value_field` and `value_fields` set.** They are mutually167 exclusive. Folio rejects.168- **Wrong relative path on `source_sheet`.** Paths are relative to169 the *calling sheet's root*, not to `derivations/`. Most sidecar170 setups use `../<other_sheet>`.171- **PK mismatch.** Trailing whitespace, case differences, or172 numeric-vs-string discrepancies will silently produce no match173 (see "No match" above). Normalize on the source side.174- **Foreign records edited but the calling sheet not re-materialized.**175 The cache flips correctly on the next run; what's wrong is letting176 agents read the stale `current_revenue_usd` in between. Re-run177 materialize after foreign updates, or schedule it.