---
name: kicad-bom-edit
description: Use when adding or editing BOM/part fields (MPN, LCSC, Manufacturer, tolerance, voltage/current ratings, DNP) on KiCad .kicad_sch symbols by hand or script, or when a kicad-cli BOM export is missing parts. Covers safe property insertion, the "malformed property silently drops a whole sheet" pitfall, and the netlist-vs-BOM count verification that catches it. KiCad 7/8/9/10 S-expression schematics.
---

# Editing KiCad schematics for BOM

Adding manufacturer part numbers (MPN), LCSC codes, ratings, etc. to a KiCad
`.kicad_sch` so the BOM can be ordered/assembled (e.g. JLCPCB). **Prefer the
KiCad GUI** (Symbol Fields Table / per-symbol properties) — it can't corrupt the
file. Only hand/script-edit when the GUI isn't available, and then follow this
exactly.

## How symbols store fields

Each placed symbol is `(symbol (lib_id ...) (at ...) ... <properties> <pins> (instances ...))`.
A field is a **property**, a direct child of the symbol:

```
(property "MPN" "FNR4018S3R3MT"
    (at 139.7 87.63 0)
    (hide yes)
    (show_name no)
    (do_not_autoplace no)
    (effects (font (size 1.27 1.27)))
)
```

Properties `Reference`, `Value`, `Footprint`, `Datasheet`, `Description` are
mandatory; `MPN`/`LCSC`/`Manufacturer`/`V`/`Type`/`Current`/etc. are custom BOM
fields. Field **names must be unique** within a symbol. `kicad-cli sch export
bom --fields '...'` selects which show up as columns.

## ⚠️ THE PITFALL: a malformed property silently drops the WHOLE sheet

New BOM fields must be **siblings** of `Value` (direct children of the symbol) —
NOT nested inside another property. If you accidentally place them *inside* the
`Value` property, `kicad-cli sch export bom` will **silently omit every component
on that sheet** — no warning, exit code 0 — while `kicad-cli sch export netlist`
still parses fine and shows nothing wrong. A whole page of parts (regulators,
connectors…) can vanish from the BOM you send to the fab.

**Root cause seen in practice:** naive regex insertion. Property indentation
**varies by file** (some use 2 tabs for `(property`, some 1). A regex like
`.*?\n\t\t\t\)` grabs the first 3-tab close — which is the inner `effects`/`font`
close, not the property's own close — so the insert lands *inside* `Value`.

## Safe insertion (script)

1. **Match the entire target property block by balanced parens**, keyed on its
   unique content (e.g. `Value` = "2.2uH" at a known `(at ...)`), from the
   `(property "Value"` line to *its own* closing paren at the property's indent
   level. Don't rely on a fixed tab count guessed from another file — detect the
   file's actual property indentation first.
2. Insert new `(property ...)` blocks **after** that close, at the **same indent
   level** as `Value` (siblings), with children one level deeper.
3. Keep every block paren-balanced (each `(property` / `(effects` / `(font` has
   its close).

Robust pattern (Python) — replace the whole Value block and append siblings:

```python
import re
src=open(f).read()
# whole L1 Value property: 2-tab property line, 3+tab children, 2-tab close
pat=re.compile(r'\t\t\(property "Value" "2\.2uH"\n(?:\t\t\t.*\n)*?\t\t\)\n')
m=pat.search(src)
def prop(n,v):
    return (f'\t\t(property "{n}" "{v}"\n\t\t\t(at 139.7 87.63 0)\n\t\t\t(hide yes)\n'
            f'\t\t\t(show_name no)\n\t\t\t(do_not_autoplace no)\n\t\t\t(effects\n\t\t\t\t(font\n'
            f'\t\t\t\t\t(size 1.27 1.27)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n')
block=m.group(0).replace('"2.2uH"','"3.3uH"')          # keep Value block intact
add=prop("MPN","FNR4018S3R3MT")+prop("LCSC","C167805")+prop("Manufacturer","cjiang")
src=src[:m.start()]+block+add+src[m.end():]
open(f,'w').write(src)
```

(Whitespace inside S-expressions is not significant to KiCad — matching indent
is only for legibility and to hit the right closing paren. KiCad rewrites
formatting on its next save.)

## ✅ ALWAYS verify: netlist component count must equal BOM row count

This is the check that catches the silent-drop bug. After any schematic edit:

```bash
kicad-cli sch export netlist --format kicadsexpr -o /tmp/n.net project.kicad_sch
kicad-cli sch export bom --fields 'Reference,Value,MPN,LCSC' -o /tmp/b.csv project.kicad_sch
# count components in each; they MUST match
```

Parse the netlist `(components (comp (ref ...)))` for the ref set, and the BOM
CSV first column for its ref set, and print the difference. If the BOM set is
smaller, some sheet was dropped — you have a malformed property. Confirm the
specific row (`grep '"L1"' /tmp/b.csv`) shows its new fields populated.

## Dual-project-instance sheets

A sub-sheet that was once its own standalone project (e.g. `Power.kicad_sch`
reused in `re-bba-rb`) has `(instances (project "power" ...) (project
"re-bba-rb" ...))` on each symbol. This is normal, but it makes such sheets
extra-sensitive to malformed properties in BOM export — always run the
count check after editing these.

## Part selection for JLCPCB / LCSC

- Fill `MPN`, `Manufacturer`, and the **`LCSC` C-number** (e.g. `C167805`) — the
  LCSC code is what JLC assembly needs. Don't invent C-numbers; verify each on
  lcsc.com / jlcpcb.com/parts (existence, stock, **Basic vs Extended** — Extended
  = small one-time fee).
- For power inductors, record and check **Isat** (saturation ≥ peak current) and
  **Irms**; a value/footprint alone isn't enough.
- Match the field-naming already used in the project (this repo uses `V`,
  `Type`, `Current`, `MPN`, `LCSC`, `Manufacturer`).
