Debug a failed folio materialize
folio materialize exits 0 even on partial failures — bad cells
are reported in the envelope's failures[]. This skill walks an
agent through reading that envelope, narrowing scope to the offending
cell(s), and re-running until failures is empty.
When this skill applies
- The user pasted a
materialize envelope where failures is non-empty.
- The user says a derived column "came back null" or "didn't update".
- The user says materialize is "stuck", "slow", or "running forever".
- A CI gate failed because
failures | length > 0.
This skill does not apply when:
- The user has a contract error (
folio validate fails). That's a
schema problem, not a materialize problem — fix contract.yaml first.
- The cells succeeded but the values are wrong. That's a prompt /
script bug — narrow with the positional target +
--ids +
--force to iterate, but it's not a "failure" in the envelope
sense.
Recap: the §10.6 envelope
Every folio materialize run prints:
{
"materialized": 12,
"skipped": 7,
"failures": [
{"record_id":"cust_006","field":"industry_tag",
"error":"...","error_type":"FolioError"}
],
"total_cost": 0.0034
}
| Field |
Meaning |
materialized |
Cells written this run. |
skipped |
Cells avoided (cache hit, respect_human_override, no foreign match for cross_sheet, etc.). |
failures |
Per-record × field errors. Other records keep going. |
total_cost |
Sum of cost_usd from ai calls. |
A non-zero exit code is not how partial failures are signalled.
failures: [] is the only success criterion.
Procedure
Capture the envelope and pretty-print it.
out=$(folio materialize ./<sheet> --actor agent:debug)
echo "$out" | jq
If failures is empty, the run is healthy — the user's complaint
is probably about values, not failures. Jump to step 6.
Group failures by (field, error_type) to see if it's one bug
or many:
echo "$out" | jq '[.failures[] | {field, error_type}] | group_by(.) |
map({key: (.[0].field + ":" + .[0].error_type), count: length})'
- All failures share
(field, error_type) → one bug; pick any
record_id to reproduce.
- Different
(field, error_type) pairs → multiple bugs; treat
each group independently.
Narrow the next run to one cell. Pick the smallest reproducible
case from step 2 — one target, one record:
folio materialize ./<sheet> <field> \
--actor agent:debug \
--ids <record_id> \
--force
folio materialize takes the target as a positional argument
(one at a time); --ids is comma-separated or repeatable.
--force ignores the cache so you re-execute even if the input
hasn't changed. Without it, after the first failure the cell may
still cache-miss but you risk wasted "skipped" runs while iterating.
Read the error and error_type. The pattern that diagnoses
most failures:
error_type (typical) |
What it usually means |
FolioError — "script exited with N" |
Python derivation crashed. Run the script directly with the row's input JSON to repro. |
FolioError — "AIClient call failed" |
Provider timeout / 5xx / rate limit. Check ANTHROPIC_API_KEY, retry; consider raising materialization.retries. |
FolioError — "missing key in output_schema" |
LLM returned JSON missing a required key. Tighten the prompt or the schema. |
FolioError — "no rows in foreign sheet" |
Misconfigured cross_sheet source_sheet path. Check it's relative to the calling sheet, not to derivations/. |
FolioError — "primary key not unique" |
The sheet itself is broken; this is upstream of materialize. Run folio validate. |
Inspect provenance to confirm the fix. After a successful re-run,
the cell should have a fresh provenance line:
folio provenance ./<sheet> <record_id> <field>
folio provenance takes the record ID and field as positional
arguments. Add --history to see every entry in the append-only
log instead of just the latest.
You should see a new at: timestamp, the matching actor:, and
for ai cells a populated model: and cost_usd: (unless the
model is unknown to the price table — that's null by design).
(If failures was empty but values look wrong.) This is a
logic bug, not a failure:
- Read the cell's provenance line — what
source produced it
(ai, python, cross_sheet, human)?
- If
source: human, the cell was edited and respect_human_override
(default true) is preserving the edit. Use --force or unset
the override on the derivation.
- If
source: cross_sheet and the value is null, there was no
foreign match — that's silent by design (see the
add-derivation-cross-sheet skill).
- Otherwise, fix the derivation (prompt / script / schema) and
repeat from step 3 with
--force.
Once the one-record case is healthy, broaden.
folio materialize ./<sheet> <field> --actor agent:debug
If failures is [], drop the positional target and run
everything.
Verify
folio materialize ./<sheet> --actor agent:debug
echo $? ; echo "$last" | jq '.failures | length'
Exit code is 0 and failures | length == 0. To turn this into a
CI gate:
out=$(folio materialize ./<sheet> --actor agent:ci)
echo "$out"
[[ "$(echo "$out" | jq '.failures | length')" == "0" ]] || exit 1
Common mistakes (don't make them)
- Trusting exit code alone.
folio materialize exits 0 with a
non-empty failures[]. Always inspect the envelope.
- Re-running without
--force while debugging. The cache may
hide your fix attempts as "skipped". Use --force until the cell
goes green, then drop it.
- Conflating "skipped" with "failed".
skipped includes cache
hits, respect_human_override skips, and "no match" for
cross_sheet — all benign. The only red signal is failures[].
- Editing a derivation file mid-debug and forgetting it invalidates
the cache. That's correct behaviour, but it means the next run
will re-execute many cells. Narrow with a positional target +
--ids during iteration.
- Filing a bug against Folio for a
python script crash. The
error field reproduces the user's script's exception verbatim;
the bug is in their derivation, not in Folio. Repro by running the
script directly with the input JSON.
See also
- The
add-derivation-ai and add-derivation-cross-sheet skills for
authoring the YAML in the first place.
folio status <sheet> for a roll-up of which derived fields are
derived vs human vs missing across the whole sheet.
1---2name: debug-failed-materialize3description: Diagnose per-cell failures from `folio materialize` — read the §10.6 envelope, locate the bad cell in `provenance.jsonl`, narrow with a positional target + `--ids`, and re-run with `--force`. Invoke when the user reports "materialize is failing", "this column came back null", "AI calls timing out", or pastes a non-empty `failures[]` list from the envelope.4---56# Debug a failed `folio materialize`78`folio materialize` exits **0** even on partial failures — bad cells9are reported in the envelope's `failures[]`. This skill walks an10agent through reading that envelope, narrowing scope to the offending11cell(s), and re-running until `failures` is empty.1213## When this skill applies1415- The user pasted a `materialize` envelope where `failures` is non-empty.16- The user says a derived column "came back null" or "didn't update".17- The user says materialize is "stuck", "slow", or "running forever".18- A CI gate failed because `failures | length > 0`.1920This skill does **not** apply when:2122- The user has a *contract* error (`folio validate` fails). That's a23 schema problem, not a materialize problem — fix `contract.yaml` first.24- The cells succeeded but the *values* are wrong. That's a prompt /25 script bug — narrow with the positional target + `--ids` +26 `--force` to iterate, but it's not a "failure" in the envelope27 sense.2829## Recap: the §10.6 envelope3031Every `folio materialize` run prints:3233```json34{35 "materialized": 12,36 "skipped": 7,37 "failures": [38 {"record_id":"cust_006","field":"industry_tag",39 "error":"...","error_type":"FolioError"}40 ],41 "total_cost": 0.003442}43```4445| Field | Meaning |46|---|---|47| `materialized` | Cells written this run. |48| `skipped` | Cells avoided (cache hit, `respect_human_override`, no foreign match for `cross_sheet`, etc.). |49| `failures` | Per-record × field errors. Other records keep going. |50| `total_cost` | Sum of `cost_usd` from `ai` calls. |5152**A non-zero exit code is not how partial failures are signalled.**53`failures: []` is the only success criterion.5455## Procedure56571. **Capture the envelope and pretty-print it.**5859 ```bash60 out=$(folio materialize ./<sheet> --actor agent:debug)61 echo "$out" | jq62 ```6364 If `failures` is empty, the run is healthy — the user's complaint65 is probably about *values*, not failures. Jump to step 6.66672. **Group failures by `(field, error_type)`** to see if it's one bug68 or many:6970 ```bash71 echo "$out" | jq '[.failures[] | {field, error_type}] | group_by(.) |72 map({key: (.[0].field + ":" + .[0].error_type), count: length})'73 ```7475 - All failures share `(field, error_type)` → one bug; pick any76 `record_id` to reproduce.77 - Different `(field, error_type)` pairs → multiple bugs; treat78 each group independently.79803. **Narrow the next run to one cell.** Pick the smallest reproducible81 case from step 2 — one target, one record:8283 ```bash84 folio materialize ./<sheet> <field> \85 --actor agent:debug \86 --ids <record_id> \87 --force88 ```8990 `folio materialize` takes the target as a positional argument91 (one at a time); `--ids` is comma-separated or repeatable.9293 `--force` ignores the cache so you re-execute even if the input94 hasn't changed. Without it, after the first failure the cell may95 still cache-miss but you risk wasted "skipped" runs while iterating.96974. **Read the `error` and `error_type`.** The pattern that diagnoses98 most failures:99100 | `error_type` (typical) | What it usually means |101 |---|---|102 | `FolioError` — "script exited with N" | Python derivation crashed. Run the script directly with the row's input JSON to repro. |103 | `FolioError` — "AIClient call failed" | Provider timeout / 5xx / rate limit. Check `ANTHROPIC_API_KEY`, retry; consider raising `materialization.retries`. |104 | `FolioError` — "missing key in output_schema" | LLM returned JSON missing a required key. Tighten the prompt or the schema. |105 | `FolioError` — "no rows in foreign sheet" | Misconfigured `cross_sheet` `source_sheet` path. Check it's relative to the calling sheet, not to `derivations/`. |106 | `FolioError` — "primary key not unique" | The sheet itself is broken; this is upstream of materialize. Run `folio validate`. |1071085. **Inspect provenance to confirm the fix.** After a successful re-run,109 the cell should have a fresh provenance line:110111 ```bash112 folio provenance ./<sheet> <record_id> <field>113 ```114115 `folio provenance` takes the record ID and field as positional116 arguments. Add `--history` to see every entry in the append-only117 log instead of just the latest.118119 You should see a new `at:` timestamp, the matching `actor:`, and120 for `ai` cells a populated `model:` and `cost_usd:` (unless the121 model is unknown to the price table — that's `null` by design).1221236. **(If `failures` was empty but values look wrong.)** This is a124 logic bug, not a failure:125126 - Read the cell's provenance line — what `source` produced it127 (`ai`, `python`, `cross_sheet`, `human`)?128 - If `source: human`, the cell was edited and `respect_human_override`129 (default `true`) is preserving the edit. Use `--force` or unset130 the override on the derivation.131 - If `source: cross_sheet` and the value is `null`, there was no132 foreign match — that's silent by design (see the133 `add-derivation-cross-sheet` skill).134 - Otherwise, fix the derivation (prompt / script / schema) and135 repeat from step 3 with `--force`.1361377. **Once the one-record case is healthy, broaden.**138139 ```bash140 folio materialize ./<sheet> <field> --actor agent:debug141 ```142143 If `failures` is `[]`, drop the positional target and run144 everything.145146## Verify147148```bash149folio materialize ./<sheet> --actor agent:debug150echo $? ; echo "$last" | jq '.failures | length'151```152153Exit code is `0` and `failures | length == 0`. To turn this into a154CI gate:155156```bash157out=$(folio materialize ./<sheet> --actor agent:ci)158echo "$out"159[[ "$(echo "$out" | jq '.failures | length')" == "0" ]] || exit 1160```161162## Common mistakes (don't make them)163164- **Trusting exit code alone.** `folio materialize` exits 0 with a165 non-empty `failures[]`. Always inspect the envelope.166- **Re-running without `--force` while debugging.** The cache may167 hide your fix attempts as "skipped". Use `--force` until the cell168 goes green, then drop it.169- **Conflating "skipped" with "failed".** `skipped` includes cache170 hits, `respect_human_override` skips, and "no match" for171 `cross_sheet` — all benign. The only red signal is `failures[]`.172- **Editing a derivation file mid-debug and forgetting it invalidates173 the cache.** That's correct behaviour, but it means the next run174 will re-execute many cells. Narrow with a positional target +175 `--ids` during iteration.176- **Filing a bug against Folio for a `python` script crash.** The177 `error` field reproduces the user's script's exception verbatim;178 the bug is in their derivation, not in Folio. Repro by running the179 script directly with the input JSON.180181## See also182183- The `add-derivation-ai` and `add-derivation-cross-sheet` skills for184 authoring the YAML in the first place.185- `folio status <sheet>` for a roll-up of which derived fields are186 derived vs human vs missing across the whole sheet.