## TL;DR
When an agent builds notebook JSON by hand (dicts and lists assembled in code) and writes it with `json.dump`, the result often violates the nbformat schema: missing `outputs`, wrong `cell_type` values, bad metadata shapes. Build with `nbformat.v4.new_notebook()`, `new_code_cell()`, and `new_markdown_cell()` instead, then `nbformat.write()`. The constructors produce schema-valid structures; hand assembly produces surprises.

```text
agent's nbformat write failed: invalid JSON, notebook won't open, parse error
```

## Use this when
- A notebook written by an agent will not open or parse.
- The agent constructs notebook dicts manually.
- `nbformat.validate()` fails on the agent's output.

## Not for this skill when
- A human's hand edit broke the JSON. Validate and repair.
- nbformat reads fail on an old file. That is a version issue.

## Steps
1. Validate the broken output: `nbformat.validate(bad_nb)`. Verify: you see the schema violations.
2. Replace hand assembly with `nbformat.v4.new_notebook()` and the `new_*_cell()` constructors. Verify: the constructed notebook validates.
3. Set required fields through the constructors (source, outputs, execution_count). Verify: no field is missing.
4. Write with `nbformat.write(nb, path)`. Verify: the file opens in Jupyter.
5. Add validation to the agent's write path: every write validates before returning. Verify: invalid notebooks can never be produced again.

## Variant phrasings
### agent write invalid notebook json
Short form. Use the constructors.

### nbformat write won't open
The symptom phrasing. Validate the output.

### new_code_cell new_notebook
The fix phrasing. Constructors, not dicts.

Compatibility: nbformat 5.x v4 API. The constructors are the stable way to build notebooks.

## Why it happens
The v4 schema has required fields that are easy to forget (code cells need `outputs` and `execution_count`, every cell needs `metadata`), and `json.dump` happily writes whatever dict you hand it. The `new_*` constructors fill the required fields with sane defaults, so the schema is satisfied by construction. Hand assembly optimizes for nothing and risks everything.

## Edge cases / pitfalls
- Copying a cell dict and mutating it can duplicate cell ids. Generate fresh ids for new cells.
- Outputs need `output_type`; a bare dict of text is not an output. Use `new_output()`.
- Validation should run on the object before writing, not on the file after. Fail before the bad file exists.
- Round-tripping through `json.loads(json.dumps(nb))` is a fine extra check for serialization safety.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_N6c52C3uH_io72F_xNWnPw
